1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Compare commits

...

18 Commits

Author SHA1 Message Date
2a9fbfdfd7 chore: send notification on successful import 2021-04-10 10:48:06 +02:00
56247b4e1e fix: throttle wakatime api requests (attempt to fix #152) 2021-04-10 10:18:09 +02:00
9d7afde6a9 chore: version 2021-04-10 00:34:37 +02:00
0df0168584 Merge branch '133-password-resets' 2021-04-10 00:34:20 +02:00
a6fe15d69b chore: add support button 2021-04-10 00:27:01 +02:00
ae363c1c82 Merge remote-tracking branch 'origin/master' 2021-04-10 00:17:13 +02:00
127a614190 Merge pull request #163 from notarock/master
Add Open Graph meta tags
2021-04-10 00:16:07 +02:00
b8cefeb595 chore: add html lang 2021-04-10 00:15:20 +02:00
ae97095688 chore: exclude health endpoint from sentry tracing 2021-04-10 00:10:16 +02:00
4706809170 feat: smtp mail provider implementation 2021-04-10 00:07:13 +02:00
ddc29f0414 chore: log mailwhale error 2021-04-09 22:51:03 +02:00
f4af787ecf Add Open Graph meta tags 2021-04-09 00:37:14 -04:00
da6a00fec5 fix: adapt tests 2021-04-05 23:00:21 +02:00
6ad33e3c3b feat: password resets (resolve #133) 2021-04-05 22:57:57 +02:00
e6e134678a wip: password resets 2021-04-05 16:25:13 +02:00
1783858854 fix: minor fixes (resolve #151) (resolve #154) 2021-04-04 10:42:27 +02:00
e1d040bd55 docs: mention wiki in docs 2021-04-04 10:23:35 +02:00
7f3a654b26 fix: import bug with small number of heartbeats (fix #160) 2021-04-04 09:45:32 +02:00
40 changed files with 1463 additions and 361 deletions

View File

@ -49,6 +49,8 @@
* [Support](#-support)
* [FAQs](#-faqs)
Further instructions can be found in the [Wiki](https://github.com/muety/wakapi/wiki).
## 📬 **User Survey**
I'd love to get some community feedback from active Wakapi users. If you want, please participate in the recent [user survey](https://github.com/muety/wakapi/issues/82). Thanks a lot!
@ -149,7 +151,7 @@ api_url = http://localhost:3000/api/heartbeat
api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
```
Optionally, you can set up a [client-side proxy](docs/advanced_setup.md) in addition.
Optionally, you can set up a [client-side proxy](https://github.com/muety/wakapi/wiki/Advanced-Setup:-Client-side-proxy) in addition.
## 🔧 Configuration Options
You can specify configuration options either via a config file (default: `config.yml`, customziable through the `-c` argument) or via environment variables. Here is an overview of all options.
@ -178,6 +180,10 @@ You can specify configuration options either via a config file (default: `config
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
| `mail.enabled` | `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
| `mail.provider` | `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
| `mail.smtp.*` | `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yaml) for details |
| `mail.mailwhale.*` | `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yaml) for details |
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |

View File

@ -1,12 +1,13 @@
env: development
server:
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6
tls_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6
tls_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https
port: 3000
base_path: /
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
app:
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
@ -38,4 +39,19 @@ sentry:
dsn: # leave blank to disable sentry integration
enable_tracing: true # whether to use performance monitoring
sample_rate: 0.75 # probability of tracing a request
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
mail:
enabled: true # whether to enable mails (used for password resets, reports, etc.)
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
smtp: # smtp settings when sending mails via smtp
host:
port:
username:
password:
tls:
sender: Wakapi <noreply@wakapi.dev>
mailwhale: # mailwhale.dev settings when using mailwhale as sending service
url:
client_id:
client_secret:

View File

@ -49,6 +49,16 @@ const (
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
)
const (
MailProviderSmtp = "smtp"
MailProviderMailWhale = "mailwhale"
)
var emailProviders = []string{
MailProviderSmtp,
MailProviderMailWhale,
}
var cfg *Config
var cFlag = flag.String("config", defaultConfigPath, "config file location")
@ -89,6 +99,7 @@ type serverConfig struct {
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"`
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"`
}
@ -100,6 +111,28 @@ type sentryConfig struct {
SampleRateHeartbeats float32 `yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS"`
}
type mailConfig struct {
Enabled bool `env:"WAKAPI_MAIL_ENABLED" default:"true"`
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
MailWhale *MailwhaleMailConfig `yaml:"mailwhale"`
Smtp *SMTPMailConfig `yaml:"smtp"`
}
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"`
}
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"`
Sender string `env:"WAKAPI_MAIL_SMTP_SENDER"`
}
type Config struct {
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
@ -108,6 +141,7 @@ type Config struct {
Db dbConfig
Server serverConfig
Sentry sentryConfig
Mail mailConfig
}
func (c *Config) CreateCookie(name, value, path string) *http.Cookie {
@ -238,6 +272,14 @@ func (c *appConfig) GetOSColors() map[string]string {
return cloneStringMap(c.Colors["operating_systems"], true)
}
func (c *serverConfig) GetPublicUrl() string {
return strings.TrimSuffix(c.PublicUrl, "/")
}
func (c *SMTPMailConfig) ConnStr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
func IsDev(env string) bool {
return env == "dev" || env == "development"
}
@ -311,7 +353,7 @@ func initSentry(config sentryConfig, debug bool) {
hub := sentry.GetHubFromContext(ctx.Span.Context())
txName := hub.Scope().Transaction()
if strings.HasPrefix(txName, "GET /assets") {
if strings.HasPrefix(txName, "GET /assets") || strings.HasPrefix(txName, "GET /api/health") {
return sentry.SampledFalse
}
if txName == "POST /api/heartbeat" {
@ -337,6 +379,15 @@ func initSentry(config sentryConfig, debug bool) {
}
}
func findString(needle string, haystack []string, defaultVal string) string {
for _, s := range haystack {
if s == needle {
return s
}
}
return defaultVal
}
func Set(config *Config) {
cfg = config
}
@ -385,6 +436,10 @@ func Load() *Config {
initSentry(config.Sentry, config.IsDev())
}
if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" {
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
}
Set(config)
return Get()
}

View File

@ -1,10 +1,12 @@
package config
const (
IndexTemplate = "index.tpl.html"
LoginTemplate = "login.tpl.html"
ImprintTemplate = "imprint.tpl.html"
SignupTemplate = "signup.tpl.html"
SettingsTemplate = "settings.tpl.html"
SummaryTemplate = "summary.tpl.html"
IndexTemplate = "index.tpl.html"
LoginTemplate = "login.tpl.html"
ImprintTemplate = "imprint.tpl.html"
SignupTemplate = "signup.tpl.html"
SetPasswordTemplate = "set-password.tpl.html"
ResetPasswordTemplate = "reset-password.tpl.html"
SettingsTemplate = "settings.tpl.html"
SummaryTemplate = "summary.tpl.html"
)

View File

@ -1,4 +1,12 @@
mode: set
github.com/muety/wakapi/models/user.go:13.13,15.2 1 1
github.com/muety/wakapi/models/user.go:78.43,81.2 1 0
github.com/muety/wakapi/models/user.go:83.45,86.2 1 0
github.com/muety/wakapi/models/user.go:88.33,93.2 1 0
github.com/muety/wakapi/models/user.go:95.41,97.2 1 0
github.com/muety/wakapi/models/user.go:99.45,101.2 1 0
github.com/muety/wakapi/models/user.go:103.45,105.2 1 0
github.com/muety/wakapi/models/user.go:107.39,109.2 1 0
github.com/muety/wakapi/models/heartbeat.go:32.34,34.2 1 1
github.com/muety/wakapi/models/heartbeat.go:36.65,37.46 1 1
github.com/muety/wakapi/models/heartbeat.go:37.46,38.46 1 1
@ -16,14 +24,16 @@ github.com/muety/wakapi/models/heartbeat.go:66.37,82.2 1 0
github.com/muety/wakapi/models/heartbeat.go:90.41,92.16 2 0
github.com/muety/wakapi/models/heartbeat.go:95.2,96.10 2 0
github.com/muety/wakapi/models/heartbeat.go:92.16,94.3 1 0
github.com/muety/wakapi/models/interval.go:39.47,40.23 1 0
github.com/muety/wakapi/models/interval.go:45.2,45.14 1 0
github.com/muety/wakapi/models/interval.go:40.23,41.13 1 0
github.com/muety/wakapi/models/interval.go:41.13,43.4 1 0
github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
github.com/muety/wakapi/models/shared.go:35.52,37.2 1 0
github.com/muety/wakapi/models/shared.go:39.52,42.16 3 0
github.com/muety/wakapi/models/shared.go:45.2,47.12 3 0
@ -90,28 +100,12 @@ github.com/muety/wakapi/models/summary.go:212.11,220.6 1 1
github.com/muety/wakapi/models/summary.go:237.33,239.2 1 1
github.com/muety/wakapi/models/summary.go:241.43,243.2 1 1
github.com/muety/wakapi/models/summary.go:245.38,247.2 1 1
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
github.com/muety/wakapi/models/alias.go:18.18,20.4 1 0
github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
github.com/muety/wakapi/models/user.go:13.13,15.2 1 1
github.com/muety/wakapi/models/user.go:67.43,70.2 1 0
github.com/muety/wakapi/models/user.go:72.33,77.2 1 0
github.com/muety/wakapi/models/user.go:79.41,81.2 1 0
github.com/muety/wakapi/models/user.go:83.45,85.2 1 0
github.com/muety/wakapi/models/user.go:87.45,89.2 1 0
github.com/muety/wakapi/models/user.go:91.39,93.2 1 0
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
@ -130,6 +124,114 @@ github.com/muety/wakapi/models/filters.go:39.8,39.27 1 1
github.com/muety/wakapi/models/filters.go:39.27,41.3 1 0
github.com/muety/wakapi/models/filters.go:41.8,41.28 1 1
github.com/muety/wakapi/models/filters.go:41.28,43.3 1 0
github.com/muety/wakapi/models/interval.go:39.47,40.23 1 0
github.com/muety/wakapi/models/interval.go:45.2,45.14 1 0
github.com/muety/wakapi/models/interval.go:40.23,41.13 1 0
github.com/muety/wakapi/models/interval.go:41.13,43.4 1 0
github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
github.com/muety/wakapi/config/config.go:134.70,136.2 1 0
github.com/muety/wakapi/config/config.go:138.65,140.2 1 0
github.com/muety/wakapi/config/config.go:142.82,152.2 1 0
github.com/muety/wakapi/config/config.go:154.31,156.2 1 0
github.com/muety/wakapi/config/config.go:158.32,160.2 1 0
github.com/muety/wakapi/config/config.go:162.74,163.19 1 0
github.com/muety/wakapi/config/config.go:164.10,165.34 1 0
github.com/muety/wakapi/config/config.go:165.34,174.4 8 0
github.com/muety/wakapi/config/config.go:178.73,179.33 1 0
github.com/muety/wakapi/config/config.go:179.33,187.17 5 0
github.com/muety/wakapi/config/config.go:191.3,192.13 2 0
github.com/muety/wakapi/config/config.go:187.17,189.4 1 0
github.com/muety/wakapi/config/config.go:196.50,197.19 1 0
github.com/muety/wakapi/config/config.go:210.2,210.12 1 0
github.com/muety/wakapi/config/config.go:198.23,202.5 1 0
github.com/muety/wakapi/config/config.go:203.26,206.5 1 0
github.com/muety/wakapi/config/config.go:207.24,208.48 1 0
github.com/muety/wakapi/config/config.go:213.53,224.2 1 1
github.com/muety/wakapi/config/config.go:226.56,228.16 2 1
github.com/muety/wakapi/config/config.go:232.2,239.3 1 1
github.com/muety/wakapi/config/config.go:228.16,230.3 1 0
github.com/muety/wakapi/config/config.go:242.54,244.2 1 1
github.com/muety/wakapi/config/config.go:246.60,248.2 1 0
github.com/muety/wakapi/config/config.go:250.59,252.2 1 0
github.com/muety/wakapi/config/config.go:254.57,256.2 1 0
github.com/muety/wakapi/config/config.go:258.53,260.2 1 0
github.com/muety/wakapi/config/config.go:262.46,264.2 1 0
github.com/muety/wakapi/config/config.go:266.29,268.2 1 1
github.com/muety/wakapi/config/config.go:270.27,272.16 2 0
github.com/muety/wakapi/config/config.go:275.2,278.16 3 0
github.com/muety/wakapi/config/config.go:282.2,282.41 1 0
github.com/muety/wakapi/config/config.go:272.16,274.3 1 0
github.com/muety/wakapi/config/config.go:278.16,280.3 1 0
github.com/muety/wakapi/config/config.go:285.48,297.16 3 0
github.com/muety/wakapi/config/config.go:300.2,302.16 3 0
github.com/muety/wakapi/config/config.go:306.2,306.55 1 0
github.com/muety/wakapi/config/config.go:310.2,310.15 1 0
github.com/muety/wakapi/config/config.go:297.16,299.3 1 0
github.com/muety/wakapi/config/config.go:302.16,304.3 1 0
github.com/muety/wakapi/config/config.go:306.55,308.3 1 0
github.com/muety/wakapi/config/config.go:313.38,314.43 1 0
github.com/muety/wakapi/config/config.go:317.2,317.15 1 0
github.com/muety/wakapi/config/config.go:314.43,316.3 1 0
github.com/muety/wakapi/config/config.go:320.45,321.27 1 0
github.com/muety/wakapi/config/config.go:324.2,324.15 1 0
github.com/muety/wakapi/config/config.go:321.27,323.3 1 0
github.com/muety/wakapi/config/config.go:327.50,331.91 1 0
github.com/muety/wakapi/config/config.go:331.91,332.29 1 0
github.com/muety/wakapi/config/config.go:336.4,339.48 3 0
github.com/muety/wakapi/config/config.go:342.4,342.39 1 0
github.com/muety/wakapi/config/config.go:345.4,345.69 1 0
github.com/muety/wakapi/config/config.go:332.29,334.5 1 0
github.com/muety/wakapi/config/config.go:339.48,341.5 1 0
github.com/muety/wakapi/config/config.go:342.39,344.5 1 0
github.com/muety/wakapi/config/config.go:347.79,351.27 2 0
github.com/muety/wakapi/config/config.go:358.4,358.16 1 0
github.com/muety/wakapi/config/config.go:351.27,352.84 1 0
github.com/muety/wakapi/config/config.go:352.84,353.57 1 0
github.com/muety/wakapi/config/config.go:353.57,355.7 1 0
github.com/muety/wakapi/config/config.go:360.17,362.3 1 0
github.com/muety/wakapi/config/config.go:365.77,366.29 1 0
github.com/muety/wakapi/config/config.go:371.2,371.19 1 0
github.com/muety/wakapi/config/config.go:366.29,367.18 1 0
github.com/muety/wakapi/config/config.go:367.18,369.4 1 0
github.com/muety/wakapi/config/config.go:374.26,376.2 1 0
github.com/muety/wakapi/config/config.go:378.20,380.2 1 0
github.com/muety/wakapi/config/config.go:382.21,387.96 3 0
github.com/muety/wakapi/config/config.go:391.2,399.52 5 0
github.com/muety/wakapi/config/config.go:403.2,403.47 1 0
github.com/muety/wakapi/config/config.go:409.2,409.70 1 0
github.com/muety/wakapi/config/config.go:413.2,413.28 1 0
github.com/muety/wakapi/config/config.go:417.2,417.29 1 0
github.com/muety/wakapi/config/config.go:422.2,422.94 1 0
github.com/muety/wakapi/config/config.go:426.2,427.14 2 0
github.com/muety/wakapi/config/config.go:387.96,389.3 1 0
github.com/muety/wakapi/config/config.go:399.52,401.3 1 0
github.com/muety/wakapi/config/config.go:403.47,404.14 1 0
github.com/muety/wakapi/config/config.go:404.14,406.4 1 0
github.com/muety/wakapi/config/config.go:409.70,411.3 1 0
github.com/muety/wakapi/config/config.go:413.28,415.3 1 0
github.com/muety/wakapi/config/config.go:417.29,420.3 2 0
github.com/muety/wakapi/config/config.go:422.94,424.3 1 0
github.com/muety/wakapi/config/utils.go:5.78,7.22 2 0
github.com/muety/wakapi/config/utils.go:13.2,13.11 1 0
github.com/muety/wakapi/config/utils.go:7.22,8.18 1 0
github.com/muety/wakapi/config/utils.go:11.3,11.12 1 0
github.com/muety/wakapi/config/utils.go:8.18,10.4 1 0
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
github.com/muety/wakapi/utils/filesystem.go:14.68,16.16 2 0
github.com/muety/wakapi/utils/filesystem.go:20.2,21.15 2 0
github.com/muety/wakapi/utils/filesystem.go:33.2,33.15 1 0
github.com/muety/wakapi/utils/filesystem.go:16.16,18.3 1 0
github.com/muety/wakapi/utils/filesystem.go:21.15,23.47 2 0
github.com/muety/wakapi/utils/filesystem.go:23.47,25.23 2 0
github.com/muety/wakapi/utils/filesystem.go:29.4,29.19 1 0
github.com/muety/wakapi/utils/filesystem.go:25.23,27.5 1 0
github.com/muety/wakapi/utils/color.go:8.90,10.32 2 0
github.com/muety/wakapi/utils/color.go:15.2,15.15 1 0
github.com/muety/wakapi/utils/color.go:10.32,11.50 1 0
@ -143,8 +245,31 @@ github.com/muety/wakapi/utils/common.go:30.24,32.2 1 0
github.com/muety/wakapi/utils/common.go:34.56,37.45 3 1
github.com/muety/wakapi/utils/common.go:40.2,40.40 1 1
github.com/muety/wakapi/utils/common.go:37.45,39.3 1 1
github.com/muety/wakapi/utils/date.go:8.31,10.2 1 0
github.com/muety/wakapi/utils/date.go:12.43,14.2 1 0
github.com/muety/wakapi/utils/date.go:16.30,20.2 3 0
github.com/muety/wakapi/utils/date.go:22.31,25.2 2 0
github.com/muety/wakapi/utils/date.go:27.30,30.2 2 0
github.com/muety/wakapi/utils/date.go:32.67,35.33 2 0
github.com/muety/wakapi/utils/date.go:44.2,44.18 1 0
github.com/muety/wakapi/utils/date.go:35.33,37.19 2 0
github.com/muety/wakapi/utils/date.go:40.3,41.10 2 0
github.com/muety/wakapi/utils/date.go:37.19,39.4 1 0
github.com/muety/wakapi/utils/date.go:47.50,53.2 5 0
github.com/muety/wakapi/utils/date.go:56.79,59.36 3 0
github.com/muety/wakapi/utils/date.go:63.2,63.21 1 0
github.com/muety/wakapi/utils/date.go:67.2,67.21 1 0
github.com/muety/wakapi/utils/date.go:71.2,71.13 1 0
github.com/muety/wakapi/utils/date.go:59.36,62.3 2 0
github.com/muety/wakapi/utils/date.go:63.21,66.3 2 0
github.com/muety/wakapi/utils/date.go:67.21,70.3 2 0
github.com/muety/wakapi/utils/http.go:9.73,12.58 3 0
github.com/muety/wakapi/utils/http.go:12.58,14.3 1 0
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
github.com/muety/wakapi/utils/summary.go:10.66,11.40 1 0
github.com/muety/wakapi/utils/summary.go:16.2,16.48 1 0
github.com/muety/wakapi/utils/summary.go:11.40,12.27 1 0
@ -183,12 +308,6 @@ github.com/muety/wakapi/utils/summary.go:96.18,98.5 1 0
github.com/muety/wakapi/utils/summary.go:112.48,116.51 2 0
github.com/muety/wakapi/utils/summary.go:119.2,119.12 1 0
github.com/muety/wakapi/utils/summary.go:116.51,118.3 1 0
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
github.com/muety/wakapi/utils/auth.go:16.79,18.54 2 0
github.com/muety/wakapi/utils/auth.go:22.2,24.16 3 0
github.com/muety/wakapi/utils/auth.go:28.2,30.45 3 0
@ -208,146 +327,6 @@ github.com/muety/wakapi/utils/auth.go:60.56,64.2 3 0
github.com/muety/wakapi/utils/auth.go:66.55,69.16 3 0
github.com/muety/wakapi/utils/auth.go:72.2,72.16 1 0
github.com/muety/wakapi/utils/auth.go:69.16,71.3 1 0
github.com/muety/wakapi/utils/date.go:8.31,10.2 1 0
github.com/muety/wakapi/utils/date.go:12.43,14.2 1 0
github.com/muety/wakapi/utils/date.go:16.30,20.2 3 0
github.com/muety/wakapi/utils/date.go:22.31,25.2 2 0
github.com/muety/wakapi/utils/date.go:27.30,30.2 2 0
github.com/muety/wakapi/utils/date.go:32.67,35.33 2 0
github.com/muety/wakapi/utils/date.go:44.2,44.18 1 0
github.com/muety/wakapi/utils/date.go:35.33,37.19 2 0
github.com/muety/wakapi/utils/date.go:40.3,41.10 2 0
github.com/muety/wakapi/utils/date.go:37.19,39.4 1 0
github.com/muety/wakapi/utils/date.go:47.50,53.2 5 0
github.com/muety/wakapi/utils/date.go:56.79,59.36 3 0
github.com/muety/wakapi/utils/date.go:63.2,63.21 1 0
github.com/muety/wakapi/utils/date.go:67.2,67.21 1 0
github.com/muety/wakapi/utils/date.go:71.2,71.13 1 0
github.com/muety/wakapi/utils/date.go:59.36,62.3 2 0
github.com/muety/wakapi/utils/date.go:63.21,66.3 2 0
github.com/muety/wakapi/utils/date.go:67.21,70.3 2 0
github.com/muety/wakapi/utils/filesystem.go:14.68,16.16 2 0
github.com/muety/wakapi/utils/filesystem.go:20.2,21.15 2 0
github.com/muety/wakapi/utils/filesystem.go:33.2,33.15 1 0
github.com/muety/wakapi/utils/filesystem.go:16.16,18.3 1 0
github.com/muety/wakapi/utils/filesystem.go:21.15,23.47 2 0
github.com/muety/wakapi/utils/filesystem.go:23.47,25.23 2 0
github.com/muety/wakapi/utils/filesystem.go:29.4,29.19 1 0
github.com/muety/wakapi/utils/filesystem.go:25.23,27.5 1 0
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
github.com/muety/wakapi/config/utils.go:5.78,7.22 2 0
github.com/muety/wakapi/config/utils.go:13.2,13.11 1 0
github.com/muety/wakapi/config/utils.go:7.22,8.18 1 0
github.com/muety/wakapi/config/utils.go:11.3,11.12 1 0
github.com/muety/wakapi/config/utils.go:8.18,10.4 1 0
github.com/muety/wakapi/config/config.go:113.70,115.2 1 0
github.com/muety/wakapi/config/config.go:117.65,119.2 1 0
github.com/muety/wakapi/config/config.go:121.82,131.2 1 0
github.com/muety/wakapi/config/config.go:133.31,135.2 1 0
github.com/muety/wakapi/config/config.go:137.32,139.2 1 0
github.com/muety/wakapi/config/config.go:141.74,142.19 1 0
github.com/muety/wakapi/config/config.go:143.10,144.34 1 0
github.com/muety/wakapi/config/config.go:144.34,153.4 8 0
github.com/muety/wakapi/config/config.go:157.73,158.33 1 0
github.com/muety/wakapi/config/config.go:158.33,166.17 5 0
github.com/muety/wakapi/config/config.go:170.3,171.13 2 0
github.com/muety/wakapi/config/config.go:166.17,168.4 1 0
github.com/muety/wakapi/config/config.go:175.50,176.19 1 0
github.com/muety/wakapi/config/config.go:189.2,189.12 1 0
github.com/muety/wakapi/config/config.go:177.23,181.5 1 0
github.com/muety/wakapi/config/config.go:182.26,185.5 1 0
github.com/muety/wakapi/config/config.go:186.24,187.48 1 0
github.com/muety/wakapi/config/config.go:192.53,203.2 1 1
github.com/muety/wakapi/config/config.go:205.56,207.16 2 1
github.com/muety/wakapi/config/config.go:211.2,218.3 1 1
github.com/muety/wakapi/config/config.go:207.16,209.3 1 0
github.com/muety/wakapi/config/config.go:221.54,223.2 1 1
github.com/muety/wakapi/config/config.go:225.60,227.2 1 0
github.com/muety/wakapi/config/config.go:229.59,231.2 1 0
github.com/muety/wakapi/config/config.go:233.57,235.2 1 0
github.com/muety/wakapi/config/config.go:237.53,239.2 1 0
github.com/muety/wakapi/config/config.go:241.29,243.2 1 1
github.com/muety/wakapi/config/config.go:245.27,247.16 2 0
github.com/muety/wakapi/config/config.go:250.2,253.16 3 0
github.com/muety/wakapi/config/config.go:257.2,257.41 1 0
github.com/muety/wakapi/config/config.go:247.16,249.3 1 0
github.com/muety/wakapi/config/config.go:253.16,255.3 1 0
github.com/muety/wakapi/config/config.go:260.48,272.16 3 0
github.com/muety/wakapi/config/config.go:275.2,277.16 3 0
github.com/muety/wakapi/config/config.go:281.2,281.55 1 0
github.com/muety/wakapi/config/config.go:285.2,285.15 1 0
github.com/muety/wakapi/config/config.go:272.16,274.3 1 0
github.com/muety/wakapi/config/config.go:277.16,279.3 1 0
github.com/muety/wakapi/config/config.go:281.55,283.3 1 0
github.com/muety/wakapi/config/config.go:288.38,289.43 1 0
github.com/muety/wakapi/config/config.go:292.2,292.15 1 0
github.com/muety/wakapi/config/config.go:289.43,291.3 1 0
github.com/muety/wakapi/config/config.go:295.45,296.27 1 0
github.com/muety/wakapi/config/config.go:299.2,299.15 1 0
github.com/muety/wakapi/config/config.go:296.27,298.3 1 0
github.com/muety/wakapi/config/config.go:302.50,306.91 1 0
github.com/muety/wakapi/config/config.go:306.91,307.29 1 0
github.com/muety/wakapi/config/config.go:311.4,314.48 3 0
github.com/muety/wakapi/config/config.go:317.4,317.39 1 0
github.com/muety/wakapi/config/config.go:320.4,320.69 1 0
github.com/muety/wakapi/config/config.go:307.29,309.5 1 0
github.com/muety/wakapi/config/config.go:314.48,316.5 1 0
github.com/muety/wakapi/config/config.go:317.39,319.5 1 0
github.com/muety/wakapi/config/config.go:322.79,326.27 2 0
github.com/muety/wakapi/config/config.go:333.4,333.16 1 0
github.com/muety/wakapi/config/config.go:326.27,327.84 1 0
github.com/muety/wakapi/config/config.go:327.84,328.57 1 0
github.com/muety/wakapi/config/config.go:328.57,330.7 1 0
github.com/muety/wakapi/config/config.go:335.17,337.3 1 0
github.com/muety/wakapi/config/config.go:340.26,342.2 1 0
github.com/muety/wakapi/config/config.go:344.20,346.2 1 0
github.com/muety/wakapi/config/config.go:348.21,353.96 3 0
github.com/muety/wakapi/config/config.go:357.2,365.52 5 0
github.com/muety/wakapi/config/config.go:369.2,369.47 1 0
github.com/muety/wakapi/config/config.go:375.2,375.70 1 0
github.com/muety/wakapi/config/config.go:379.2,379.28 1 0
github.com/muety/wakapi/config/config.go:383.2,383.29 1 0
github.com/muety/wakapi/config/config.go:388.2,389.14 2 0
github.com/muety/wakapi/config/config.go:353.96,355.3 1 0
github.com/muety/wakapi/config/config.go:365.52,367.3 1 0
github.com/muety/wakapi/config/config.go:369.47,370.14 1 0
github.com/muety/wakapi/config/config.go:370.14,372.4 1 0
github.com/muety/wakapi/config/config.go:375.70,377.3 1 0
github.com/muety/wakapi/config/config.go:379.28,381.3 1 0
github.com/muety/wakapi/config/config.go:383.29,386.3 2 0
github.com/muety/wakapi/middlewares/authenticate.go:19.91,25.2 1 1
github.com/muety/wakapi/middlewares/authenticate.go:27.90,30.2 2 0
github.com/muety/wakapi/middlewares/authenticate.go:32.90,35.2 2 0
github.com/muety/wakapi/middlewares/authenticate.go:37.71,38.71 1 0
github.com/muety/wakapi/middlewares/authenticate.go:38.71,40.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:43.107,47.16 3 0
github.com/muety/wakapi/middlewares/authenticate.go:51.2,51.31 1 0
github.com/muety/wakapi/middlewares/authenticate.go:67.2,68.12 2 0
github.com/muety/wakapi/middlewares/authenticate.go:47.16,49.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:51.31,52.31 1 0
github.com/muety/wakapi/middlewares/authenticate.go:57.3,57.29 1 0
github.com/muety/wakapi/middlewares/authenticate.go:64.3,64.9 1 0
github.com/muety/wakapi/middlewares/authenticate.go:52.31,55.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:57.29,60.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:60.9,63.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:71.70,72.39 1 0
github.com/muety/wakapi/middlewares/authenticate.go:77.2,77.14 1 0
github.com/muety/wakapi/middlewares/authenticate.go:72.39,73.60 1 0
github.com/muety/wakapi/middlewares/authenticate.go:73.60,75.4 1 0
github.com/muety/wakapi/middlewares/authenticate.go:80.92,82.16 2 1
github.com/muety/wakapi/middlewares/authenticate.go:86.2,89.16 4 1
github.com/muety/wakapi/middlewares/authenticate.go:92.2,92.18 1 1
github.com/muety/wakapi/middlewares/authenticate.go:82.16,84.3 1 1
github.com/muety/wakapi/middlewares/authenticate.go:89.16,91.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:95.92,97.16 2 0
github.com/muety/wakapi/middlewares/authenticate.go:101.2,102.16 2 0
github.com/muety/wakapi/middlewares/authenticate.go:109.2,109.18 1 0
github.com/muety/wakapi/middlewares/authenticate.go:97.16,99.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:102.16,104.3 1 0
github.com/muety/wakapi/middlewares/filetype.go:13.83,14.43 1 0
github.com/muety/wakapi/middlewares/filetype.go:14.43,19.3 1 0
github.com/muety/wakapi/middlewares/filetype.go:22.84,24.34 2 0
@ -396,6 +375,35 @@ github.com/muety/wakapi/middlewares/sentry.go:15.43,19.3 1 0
github.com/muety/wakapi/middlewares/sentry.go:22.78,25.54 3 0
github.com/muety/wakapi/middlewares/sentry.go:25.54,26.43 1 0
github.com/muety/wakapi/middlewares/sentry.go:26.43,28.4 1 0
github.com/muety/wakapi/middlewares/authenticate.go:19.91,25.2 1 1
github.com/muety/wakapi/middlewares/authenticate.go:27.90,30.2 2 0
github.com/muety/wakapi/middlewares/authenticate.go:32.90,35.2 2 0
github.com/muety/wakapi/middlewares/authenticate.go:37.71,38.71 1 0
github.com/muety/wakapi/middlewares/authenticate.go:38.71,40.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:43.107,47.16 3 0
github.com/muety/wakapi/middlewares/authenticate.go:51.2,51.31 1 0
github.com/muety/wakapi/middlewares/authenticate.go:67.2,68.12 2 0
github.com/muety/wakapi/middlewares/authenticate.go:47.16,49.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:51.31,52.31 1 0
github.com/muety/wakapi/middlewares/authenticate.go:57.3,57.29 1 0
github.com/muety/wakapi/middlewares/authenticate.go:64.3,64.9 1 0
github.com/muety/wakapi/middlewares/authenticate.go:52.31,55.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:57.29,60.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:60.9,63.4 2 0
github.com/muety/wakapi/middlewares/authenticate.go:71.70,72.39 1 0
github.com/muety/wakapi/middlewares/authenticate.go:77.2,77.14 1 0
github.com/muety/wakapi/middlewares/authenticate.go:72.39,73.60 1 0
github.com/muety/wakapi/middlewares/authenticate.go:73.60,75.4 1 0
github.com/muety/wakapi/middlewares/authenticate.go:80.92,82.16 2 1
github.com/muety/wakapi/middlewares/authenticate.go:86.2,89.16 4 1
github.com/muety/wakapi/middlewares/authenticate.go:92.2,92.18 1 1
github.com/muety/wakapi/middlewares/authenticate.go:82.16,84.3 1 1
github.com/muety/wakapi/middlewares/authenticate.go:89.16,91.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:95.92,97.16 2 0
github.com/muety/wakapi/middlewares/authenticate.go:101.2,102.16 2 0
github.com/muety/wakapi/middlewares/authenticate.go:109.2,109.18 1 0
github.com/muety/wakapi/middlewares/authenticate.go:97.16,99.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:102.16,104.3 1 0
github.com/muety/wakapi/services/alias.go:17.77,22.2 1 1
github.com/muety/wakapi/services/alias.go:26.60,27.43 1 1
github.com/muety/wakapi/services/alias.go:30.2,30.14 1 1
@ -431,72 +439,6 @@ github.com/muety/wakapi/services/alias.go:95.21,97.4 1 0
github.com/muety/wakapi/services/alias.go:104.31,106.3 1 0
github.com/muety/wakapi/services/alias.go:111.52,112.51 1 0
github.com/muety/wakapi/services/alias.go:112.51,114.3 1 0
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
github.com/muety/wakapi/services/heartbeat.go:25.72,27.2 1 0
github.com/muety/wakapi/services/heartbeat.go:29.80,34.32 3 0
github.com/muety/wakapi/services/heartbeat.go:41.2,41.55 1 0
github.com/muety/wakapi/services/heartbeat.go:34.32,35.36 1 0
github.com/muety/wakapi/services/heartbeat.go:35.36,38.4 2 0
github.com/muety/wakapi/services/heartbeat.go:44.53,46.2 1 0
github.com/muety/wakapi/services/heartbeat.go:48.76,50.2 1 0
github.com/muety/wakapi/services/heartbeat.go:52.96,54.2 1 0
github.com/muety/wakapi/services/heartbeat.go:56.111,58.16 2 0
github.com/muety/wakapi/services/heartbeat.go:61.2,61.43 1 0
github.com/muety/wakapi/services/heartbeat.go:58.16,60.3 1 0
github.com/muety/wakapi/services/heartbeat.go:64.116,66.2 1 0
github.com/muety/wakapi/services/heartbeat.go:68.78,70.2 1 0
github.com/muety/wakapi/services/heartbeat.go:72.62,74.2 1 0
github.com/muety/wakapi/services/heartbeat.go:76.116,78.16 2 0
github.com/muety/wakapi/services/heartbeat.go:82.2,82.28 1 0
github.com/muety/wakapi/services/heartbeat.go:86.2,86.24 1 0
github.com/muety/wakapi/services/heartbeat.go:78.16,80.3 1 0
github.com/muety/wakapi/services/heartbeat.go:82.28,84.3 1 0
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
github.com/muety/wakapi/services/user.go:19.73,25.2 1 0
github.com/muety/wakapi/services/user.go:27.74,28.40 1 0
github.com/muety/wakapi/services/user.go:32.2,33.16 2 0
github.com/muety/wakapi/services/user.go:37.2,38.15 2 0
github.com/muety/wakapi/services/user.go:28.40,30.3 1 0
github.com/muety/wakapi/services/user.go:33.16,35.3 1 0
github.com/muety/wakapi/services/user.go:41.72,42.37 1 0
github.com/muety/wakapi/services/user.go:46.2,47.16 2 0
github.com/muety/wakapi/services/user.go:51.2,52.15 2 0
github.com/muety/wakapi/services/user.go:42.37,44.3 1 0
github.com/muety/wakapi/services/user.go:47.16,49.3 1 0
github.com/muety/wakapi/services/user.go:55.58,57.2 1 0
github.com/muety/wakapi/services/user.go:59.61,62.2 2 0
github.com/muety/wakapi/services/user.go:64.48,66.2 1 0
github.com/muety/wakapi/services/user.go:68.102,77.93 2 0
github.com/muety/wakapi/services/user.go:83.2,83.38 1 0
github.com/muety/wakapi/services/user.go:77.93,79.3 1 0
github.com/muety/wakapi/services/user.go:79.8,81.3 1 0
github.com/muety/wakapi/services/user.go:86.73,89.2 2 0
github.com/muety/wakapi/services/user.go:91.78,95.2 3 0
github.com/muety/wakapi/services/user.go:97.99,100.2 2 0
github.com/muety/wakapi/services/user.go:102.106,105.96 3 0
github.com/muety/wakapi/services/user.go:110.2,110.68 1 0
github.com/muety/wakapi/services/user.go:105.96,107.3 1 0
github.com/muety/wakapi/services/user.go:107.8,109.3 1 0
github.com/muety/wakapi/services/user.go:113.57,116.2 2 0
github.com/muety/wakapi/services/user.go:118.38,120.2 1 0
github.com/muety/wakapi/services/aggregation.go:24.142,31.2 1 0
github.com/muety/wakapi/services/aggregation.go:40.43,42.37 1 0
github.com/muety/wakapi/services/aggregation.go:46.2,48.19 3 0
@ -537,6 +479,52 @@ github.com/muety/wakapi/services/aggregation.go:136.62,140.4 1 0
github.com/muety/wakapi/services/aggregation.go:148.83,163.41 5 0
github.com/muety/wakapi/services/aggregation.go:163.41,173.3 3 0
github.com/muety/wakapi/services/aggregation.go:176.34,179.2 2 0
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
github.com/muety/wakapi/services/heartbeat.go:25.72,27.2 1 0
github.com/muety/wakapi/services/heartbeat.go:29.80,34.32 3 0
github.com/muety/wakapi/services/heartbeat.go:41.2,41.55 1 0
github.com/muety/wakapi/services/heartbeat.go:34.32,35.36 1 0
github.com/muety/wakapi/services/heartbeat.go:35.36,38.4 2 0
github.com/muety/wakapi/services/heartbeat.go:44.53,46.2 1 0
github.com/muety/wakapi/services/heartbeat.go:48.76,50.2 1 0
github.com/muety/wakapi/services/heartbeat.go:52.96,54.2 1 0
github.com/muety/wakapi/services/heartbeat.go:56.111,58.16 2 0
github.com/muety/wakapi/services/heartbeat.go:61.2,61.43 1 0
github.com/muety/wakapi/services/heartbeat.go:58.16,60.3 1 0
github.com/muety/wakapi/services/heartbeat.go:64.116,66.2 1 0
github.com/muety/wakapi/services/heartbeat.go:68.78,70.2 1 0
github.com/muety/wakapi/services/heartbeat.go:72.62,74.2 1 0
github.com/muety/wakapi/services/heartbeat.go:76.116,78.16 2 0
github.com/muety/wakapi/services/heartbeat.go:82.2,82.28 1 0
github.com/muety/wakapi/services/heartbeat.go:86.2,86.24 1 0
github.com/muety/wakapi/services/heartbeat.go:78.16,80.3 1 0
github.com/muety/wakapi/services/heartbeat.go:82.28,84.3 1 0
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
github.com/muety/wakapi/services/misc.go:23.126,30.2 1 0
github.com/muety/wakapi/services/misc.go:42.50,44.48 1 0
github.com/muety/wakapi/services/misc.go:48.2,50.19 3 0
@ -649,10 +637,33 @@ github.com/muety/wakapi/services/summary.go:324.54,326.3 1 1
github.com/muety/wakapi/services/summary.go:331.59,333.25 2 1
github.com/muety/wakapi/services/summary.go:336.2,336.32 1 1
github.com/muety/wakapi/services/summary.go:333.25,335.3 1 1
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
github.com/muety/wakapi/services/user.go:19.73,25.2 1 0
github.com/muety/wakapi/services/user.go:27.74,28.40 1 0
github.com/muety/wakapi/services/user.go:32.2,33.16 2 0
github.com/muety/wakapi/services/user.go:37.2,38.15 2 0
github.com/muety/wakapi/services/user.go:28.40,30.3 1 0
github.com/muety/wakapi/services/user.go:33.16,35.3 1 0
github.com/muety/wakapi/services/user.go:41.72,42.37 1 0
github.com/muety/wakapi/services/user.go:46.2,47.16 2 0
github.com/muety/wakapi/services/user.go:51.2,52.15 2 0
github.com/muety/wakapi/services/user.go:42.37,44.3 1 0
github.com/muety/wakapi/services/user.go:47.16,49.3 1 0
github.com/muety/wakapi/services/user.go:55.76,57.2 1 0
github.com/muety/wakapi/services/user.go:59.86,61.2 1 0
github.com/muety/wakapi/services/user.go:63.58,65.2 1 0
github.com/muety/wakapi/services/user.go:67.61,70.2 2 0
github.com/muety/wakapi/services/user.go:72.48,74.2 1 0
github.com/muety/wakapi/services/user.go:76.102,85.93 2 0
github.com/muety/wakapi/services/user.go:91.2,91.38 1 0
github.com/muety/wakapi/services/user.go:85.93,87.3 1 0
github.com/muety/wakapi/services/user.go:87.8,89.3 1 0
github.com/muety/wakapi/services/user.go:94.73,97.2 2 0
github.com/muety/wakapi/services/user.go:99.78,103.2 3 0
github.com/muety/wakapi/services/user.go:105.99,108.2 2 0
github.com/muety/wakapi/services/user.go:110.106,113.96 3 0
github.com/muety/wakapi/services/user.go:118.2,118.68 1 0
github.com/muety/wakapi/services/user.go:113.96,115.3 1 0
github.com/muety/wakapi/services/user.go:115.8,117.3 1 0
github.com/muety/wakapi/services/user.go:121.85,123.2 1 0
github.com/muety/wakapi/services/user.go:125.57,128.2 2 0
github.com/muety/wakapi/services/user.go:130.38,132.2 1 0

View File

@ -1,47 +0,0 @@
# Advanced Setup
This page contains instructions for additional setup options, none of which are mandatory.
## Optional: Client-side proxy
Most Wakatime plugins work in a way that, for every heartbeat to send, the plugin calls your local [wakatime-cli](https://github.com/wakatime/wakatime) (a small Python program that is automatically installed when installing a Wakatime plugin) with a few command-line arguments, which is then run as a new process. Inside that process, a heartbeat request is forged and sent to the backend API Wakapi in this case.
While this is convenient for plugin developers, as they do not have to deal with sending HTTP requests, etc., it comes with a minor drawback. Because the CLI process shuts down after each request, its TCP connection is closed as well. Accordingly, **TCP connections cannot be re-used** and every single heartbeat request is inevitably preceded by the `SYN` + `SYN ACK` + `ACK` sequence for establishing a new TCP connection as well as a handshake for establishing a new TLS session.
While this certainly does not hurt, it is still a bit of overhead. You can avoid that by setting up a local reverse proxy on your machine, that keeps running as a daemon and can therefore keep a continuous connection.
### Option 1: [tinyproxy](https://tinyproxy.github.io) forward proxy (`Linux`, `Mac` only)
In this example we use _tinyproxy_ as a small, easy-to-install proxy server, written in C, that runs on your local machine.
1. Install [tinyproxy](https://tinyproxy.github.io)
* Fedora / RHEL: `dnf install tinyproxy`
* Debian / Ubuntu: `apt install tinyproxy`
* MacOS: Install from [MacPorts](https://ports.macports.org/port/tinyproxy/summary)
1. Enable and start it
* Linux: `sudo systemctl start tinyproxy && sudo systemctl enable tinyproxy`
* Mac: Not sure, sorry ¯\_(ツ)_/¯
1. Update `~/.wakatime.cfg`
* Set `proxy = http://localhost:8888`
1. Done
* All Wakapi requests are passed through tinyproxy now, which keeps a TCP connection with the server open for some time
### Option 2: [Caddy](https://caddyserver.com) reverse proxy (`Win`, `Linux`, `Mac`)
In this example, we misuse Caddy, which is a web server and reverse proxy, to fulfil the above scenario.
1. [Install Caddy](https://caddyserver.com/)
* When installing manually, don't forget to set up a systemd service to start Caddy on system startup
1. Create a Caddyfile
```
# /etc/caddy/Caddyfile
http://localhost:8070 {
reverse_proxy * {
to https://wakapi.dev # <-- substitute your own Wakapi host here
header_up Host {http.reverse_proxy.upstream.host}
header_down -Server
}
}
```
1. Restart Caddy
1. Verify that you can access [`http://localhost:8070/api/health`](http://localhost:8070/api/health)
1. Update `~/.wakatime.cfg`
* Set `api_url = http://localhost:8070/api/heartbeat`
1. Done
* All Wakapi requests are passed through Caddy now, which keeps a TCP connection with the server open for some time

2
go.mod
View File

@ -4,6 +4,8 @@ go 1.13
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.15.0
github.com/emvi/logbuch v1.1.1
github.com/getsentry/sentry-go v0.10.0
github.com/go-co-op/gocron v0.3.3

7
go.sum
View File

@ -77,6 +77,10 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
@ -99,6 +103,7 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-co-op/gocron v0.3.3 h1:QnarcMZWWKrEP25uCbtDiLsnnGw+PhCjL3wNITdWJOs=
github.com/go-co-op/gocron v0.3.3/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -166,6 +171,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -408,6 +414,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=

View File

@ -10,6 +10,7 @@ import (
"github.com/muety/wakapi/migrations"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/routes/api"
"github.com/muety/wakapi/services/mail"
"github.com/muety/wakapi/utils"
"gorm.io/gorm/logger"
"log"
@ -51,6 +52,7 @@ var (
languageMappingService services.ILanguageMappingService
summaryService services.ISummaryService
aggregationService services.IAggregationService
mailService services.IMailService
keyValueService services.IKeyValueService
miscService services.IMiscService
)
@ -134,6 +136,7 @@ func main() {
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
mailService = mail.NewMailService()
keyValueService = services.NewKeyValueService(keyValueRepository)
miscService = services.NewMiscService(userService, summaryService, keyValueService)
@ -157,9 +160,9 @@ func main() {
// MVC Handlers
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService, mailService)
homeHandler := routes.NewHomeHandler(keyValueService)
loginHandler := routes.NewLoginHandler(userService)
loginHandler := routes.NewLoginHandler(userService, mailService)
imprintHandler := routes.NewImprintHandler(keyValueService)
// Setup Routers

View File

@ -19,6 +19,16 @@ func (m *UserServiceMock) GetUserByKey(s string) (*models.User, error) {
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetUserByEmail(s string) (*models.User, error) {
args := m.Called(s)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetUserByResetToken(s string) (*models.User, error) {
args := m.Called(s)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetAll() ([]*models.User, error) {
args := m.Called()
return args.Get(0).([]*models.User), args.Error(1)
@ -69,6 +79,11 @@ func (m *UserServiceMock) MigrateMd5Password(user *models.User, login *models.Lo
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GenerateResetToken(user *models.User) (*models.User, error) {
args := m.Called(user)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) FlushCache() {
m.Called()
}

45
models/mail.go Normal file
View File

@ -0,0 +1,45 @@
package models
import (
"fmt"
"strings"
)
type Mail struct {
From MailAddress
To MailAddresses
Subject string
Body string
Type string
}
func (m *Mail) WithText(text string) *Mail {
m.Body = text
m.Type = "text/plain; charset=UTF-8"
return m
}
func (m *Mail) WithHTML(html string) *Mail {
m.Body = html
m.Type = "text/html; charset=UTF-8"
return m
}
func (m *Mail) String() string {
return fmt.Sprintf("To: %s\r\n"+
"From: %s\r\n"+
"Subject: %s\r\n"+
"Content-Type: %s\r\n"+
"\r\n"+
"%s\r\n",
strings.Join(m.To.RawStrings(), ", "),
m.From.String(),
m.Subject,
m.Type,
m.Body,
)
}
func (m *Mail) Reader() *strings.Reader {
return strings.NewReader(m.String())
}

66
models/mail_address.go Normal file
View File

@ -0,0 +1,66 @@
package models
import "regexp"
const (
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
EmailAddrPattern = ".*\\s<(" + MailPattern + ")>|(" + MailPattern + ")"
)
var (
mailRegex *regexp.Regexp
emailAddrRegex *regexp.Regexp
)
func init() {
mailRegex = regexp.MustCompile(MailPattern)
emailAddrRegex = regexp.MustCompile(EmailAddrPattern)
}
type MailAddress string
type MailAddresses []MailAddress
func (m MailAddress) String() string {
return string(m)
}
func (m MailAddress) Raw() string {
match := emailAddrRegex.FindStringSubmatch(string(m))
if len(match) == 3 {
if match[2] != "" {
return match[2]
}
return match[1]
}
return ""
}
func (m MailAddress) Valid() bool {
return emailAddrRegex.Match([]byte(m))
}
func (m MailAddresses) Strings() []string {
out := make([]string, len(m))
for i, s := range m {
out[i] = s.String()
}
return out
}
func (m MailAddresses) RawStrings() []string {
out := make([]string, len(m))
for i, s := range m {
out[i] = s.Raw()
}
return out
}
func (m MailAddresses) AllValid() bool {
for _, a := range m {
if !a.Valid() {
return false
}
}
return true
}

View File

@ -2,14 +2,6 @@ package models
import "regexp"
const (
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
)
var (
mailRegex *regexp.Regexp
)
func init() {
mailRegex = regexp.MustCompile(MailPattern)
}
@ -17,7 +9,7 @@ func init() {
type User struct {
ID string `json:"id" gorm:"primary_key"`
ApiKey string `json:"api_key" gorm:"unique"`
Email string `json:"email"`
Email string `json:"email" gorm:"uniqueIndex:idx_user_email"`
Password string `json:"-"`
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
@ -30,6 +22,7 @@ type User struct {
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
HasData bool `json:"-" gorm:"default:false; type:bool"`
WakatimeApiKey string `json:"-"`
ResetToken string `json:"-"`
}
type Login struct {
@ -44,6 +37,16 @@ type Signup struct {
PasswordRepeat string `schema:"password_repeat"`
}
type SetPasswordRequest struct {
Password string `schema:"password"`
PasswordRepeat string `schema:"password_repeat"`
Token string `schema:"token"`
}
type ResetPasswordRequest struct {
Email string `schema:"email"`
}
type CredentialsReset struct {
PasswordOld string `schema:"password_old"`
PasswordNew string `schema:"password_new"`
@ -69,6 +72,11 @@ func (c *CredentialsReset) IsValid() bool {
c.PasswordNew == c.PasswordRepeat
}
func (c *SetPasswordRequest) IsValid() bool {
return ValidatePassword(c.Password) &&
c.Password == c.PasswordRepeat
}
func (s *Signup) IsValid() bool {
return ValidateUsername(s.Username) &&
ValidateEmail(s.Email) &&

View File

@ -6,6 +6,11 @@ type LoginViewModel struct {
TotalUsers int
}
type SetPasswordViewModel struct {
LoginViewModel
Token string
}
func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel {
s.Success = m
return s

View File

@ -2,7 +2,6 @@ package repositories
import (
"errors"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -40,10 +39,6 @@ func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
return err
}
if result.RowsAffected != 1 {
logbuch.Warn("did not insert key '%s', maybe just updated?", kv.Key)
}
return nil
}

View File

@ -51,6 +51,8 @@ type IUserRepository interface {
GetById(string) (*models.User, error)
GetByIds([]string) ([]*models.User, error)
GetByApiKey(string) (*models.User, error)
GetByEmail(string) (*models.User, error)
GetByResetToken(string) (*models.User, error)
GetAll() ([]*models.User, error)
GetByLoggedInAfter(time.Time) ([]*models.User, error)
GetByLastActiveAfter(time.Time) ([]*models.User, error)

View File

@ -42,6 +42,28 @@ func (r *UserRepository) GetByApiKey(key string) (*models.User, error) {
return u, nil
}
func (r *UserRepository) GetByResetToken(resetToken string) (*models.User, error) {
if resetToken == "" {
return nil, errors.New("invalid input")
}
u := &models.User{}
if err := r.db.Where(&models.User{ResetToken: resetToken}).First(u).Error; err != nil {
return u, err
}
return u, nil
}
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
if email == "" {
return nil, errors.New("invalid input")
}
u := &models.User{}
if err := r.db.Where(&models.User{Email: email}).First(u).Error; err != nil {
return u, err
}
return u, nil
}
func (r *UserRepository) GetAll() ([]*models.User, error) {
var users []*models.User
if err := r.db.
@ -119,6 +141,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
"share_machines": user.ShareMachines,
"wakatime_api_key": user.WakatimeApiKey,
"has_data": user.HasData,
"reset_token": user.ResetToken,
}
result := r.db.Model(user).Updates(updateMap)

View File

@ -20,6 +20,7 @@ type HomeHandler struct {
var loginDecoder = schema.NewDecoder()
var signupDecoder = schema.NewDecoder()
var resetPasswordDecoder = schema.NewDecoder()
func NewHomeHandler(keyValueService services.IKeyValueService) *HomeHandler {
return &HomeHandler{

View File

@ -2,6 +2,7 @@ package routes
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
@ -9,18 +10,21 @@ import (
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"net/url"
"time"
)
type LoginHandler struct {
config *conf.Config
userSrvc services.IUserService
mailSrvc services.IMailService
}
func NewLoginHandler(userService services.IUserService) *LoginHandler {
func NewLoginHandler(userService services.IUserService, mailService services.IMailService) *LoginHandler {
return &LoginHandler{
config: conf.Get(),
userSrvc: userService,
mailSrvc: mailService,
}
}
@ -30,6 +34,10 @@ func (h *LoginHandler) RegisterRoutes(router *mux.Router) {
router.Path("/logout").Methods(http.MethodPost).HandlerFunc(h.PostLogout)
router.Path("/signup").Methods(http.MethodGet).HandlerFunc(h.GetSignup)
router.Path("/signup").Methods(http.MethodPost).HandlerFunc(h.PostSignup)
router.Path("/set-password").Methods(http.MethodGet).HandlerFunc(h.GetSetPassword)
router.Path("/set-password").Methods(http.MethodPost).HandlerFunc(h.PostSetPassword)
router.Path("/reset-password").Methods(http.MethodGet).HandlerFunc(h.GetResetPassword)
router.Path("/reset-password").Methods(http.MethodPost).HandlerFunc(h.PostResetPassword)
}
func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
@ -167,6 +175,128 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound)
}
func (h *LoginHandler) GetResetPassword(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r))
}
func (h *LoginHandler) GetSetPassword(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
values, _ := url.ParseQuery(r.URL.RawQuery)
token := values.Get("token")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid or missing token"))
return
}
vm := &view.SetPasswordViewModel{
LoginViewModel: *h.buildViewModel(r),
Token: token,
}
templates[conf.SetPasswordTemplate].Execute(w, vm)
}
func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
var setRequest models.SetPasswordRequest
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
if err := signupDecoder.Decode(&setRequest, r.PostForm); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
user, err := h.userSrvc.GetUserByResetToken(setRequest.Token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid token"))
return
}
if !setRequest.IsValid() {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
return
}
user.Password = setRequest.Password
user.ResetToken = ""
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
return
} else {
user.Password = hash
}
if _, err := h.userSrvc.Update(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
return
}
http.Redirect(w, r, fmt.Sprintf("%s/login?success=%s", h.config.Server.BasePath, "password updated successfully"), http.StatusFound)
}
func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
if !h.config.Mail.Enabled {
w.WriteHeader(http.StatusNotImplemented)
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("mailing is disabled on this server"))
return
}
var resetRequest models.ResetPasswordRequest
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
if err := resetPasswordDecoder.Decode(&resetRequest, r.PostForm); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
return
} else {
go func(user *models.User) {
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
if err := h.mailSrvc.SendPasswordReset(user, link); err != nil {
logbuch.Error("failed to send password reset mail to %s %v", user.ID, err)
} else {
logbuch.Info("sent password reset mail to %s", user.ID)
}
}(u)
}
} else {
logbuch.Warn("password reset requested for unregistered address '%s'", resetRequest.Email)
}
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "an e-mail was sent to you in case your e-mail address was registered"), http.StatusFound)
}
func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
numUsers, _ := h.userSrvc.Count()

View File

@ -27,6 +27,7 @@ type SettingsHandler struct {
aggregationSrvc services.IAggregationService
languageMappingSrvc services.ILanguageMappingService
keyValueSrvc services.IKeyValueService
mailSrvc services.IMailService
httpClient *http.Client
}
@ -40,6 +41,7 @@ func NewSettingsHandler(
aggregationService services.IAggregationService,
languageMappingService services.ILanguageMappingService,
keyValueService services.IKeyValueService,
mailService services.IMailService,
) *SettingsHandler {
return &SettingsHandler{
config: conf.Get(),
@ -50,6 +52,7 @@ func NewSettingsHandler(
userSrvc: userService,
heartbeatSrvc: heartbeatService,
keyValueSrvc: keyValueService,
mailSrvc: mailService,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
@ -401,6 +404,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
}
go func(user *models.User) {
start := time.Now()
importer := imports.NewWakatimeHeartbeatImporter(user.WakatimeApiKey)
countBefore, err := h.heartbeatSrvc.CountByUser(user)
@ -419,23 +423,38 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
count := 0
batch := make([]*models.Heartbeat, 0)
insert := func(batch []*models.Heartbeat) {
if err := h.heartbeatSrvc.InsertBatch(batch); err != nil {
logbuch.Warn("failed to insert imported heartbeat, already existing? %v", err)
}
}
for hb := range stream {
count++
batch = append(batch, hb)
if len(batch) == h.config.App.ImportBatchSize {
if err := h.heartbeatSrvc.InsertBatch(batch); err != nil {
logbuch.Warn("failed to insert imported heartbeat, already existing? %v", err)
}
insert(batch)
batch = make([]*models.Heartbeat, 0)
}
}
if len(batch) > 0 {
insert(batch)
}
countAfter, _ := h.heartbeatSrvc.CountByUser(user)
logbuch.Info("downloaded %d heartbeats for user '%s' (%d actually imported)", count, user.ID, countAfter-countBefore)
h.regenerateSummaries(user)
if user.Email != "" {
if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil {
logbuch.Error("failed to send import notification mail to %s %v", user.ID, err)
} else {
logbuch.Info("sent import notification mail to %s", user.ID)
}
}
}(user)
h.keyValueSrvc.PutString(&models.KeyStringValue{
@ -443,7 +462,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
Value: time.Now().Format(time.RFC822),
})
return http.StatusAccepted, "ImportAll started. This may take a few minutes.", ""
return http.StatusAccepted, "Import started. This will take several minutes. Please check back later.", ""
}
func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *http.Request) (int, string, string) {

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
@ -17,7 +18,13 @@ import (
)
const OriginWakatime = "wakatime"
const maxWorkers = 6
const (
// wakatime api permits a max. rate of 10 req / sec
// https://github.com/wakatime/wakatime/issues/261
// with 5 workers, each sleeping slightly over 1/2 sec after every req., we should stay well below that limit
maxWorkers = 5
throttleDelay = 550 * time.Millisecond
)
type WakatimeHeartbeatImporter struct {
ApiKey string
@ -72,11 +79,12 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time,
go func(day time.Time) {
defer sem.Release(1)
defer time.Sleep(throttleDelay)
d := day.Format(config.SimpleDateFormat)
heartbeats, err := w.fetchHeartbeats(d)
if err != nil {
logbuch.Error("failed to fetch heartbeats for day '%s' and user '%s' &v", day, user.ID, err)
logbuch.Error("failed to fetch heartbeats for day '%s' and user '%s' &v", d, user.ID, err)
}
for _, h := range heartbeats {
@ -113,6 +121,8 @@ func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.Hea
res, err := httpClient.Do(w.withHeaders(req))
if err != nil {
return nil, err
} else if res.StatusCode >= 400 {
return nil, errors.New(fmt.Sprintf("got status %d from wakatime api", res.StatusCode))
}
var heartbeatsData wakatime.HeartbeatsViewModel

81
services/mail/mail.go Normal file
View File

@ -0,0 +1,81 @@
package mail
import (
"bytes"
"fmt"
"github.com/markbates/pkger"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/services"
"io/ioutil"
"text/template"
)
const (
tplPath = "/views/mail"
tplNamePasswordReset = "reset_password"
tplNameImportNotification = "import_finished"
subjectPasswordReset = "Wakapi Password Reset"
subjectImportNotification = "Wakapi Data Import Finished"
)
type PasswordResetTplData struct {
ResetLink string
}
type ImportNotificationTplData struct {
PublicUrl string
Duration string
NumHeartbeats int
}
// Factory
func NewMailService() services.IMailService {
config := conf.Get()
if config.Mail.Enabled {
if config.Mail.Provider == conf.MailProviderMailWhale {
return NewMailWhaleService(config.Mail.MailWhale, config.Server.PublicUrl)
} else if config.Mail.Provider == conf.MailProviderSmtp {
return NewSMTPMailService(config.Mail.Smtp, config.Server.PublicUrl)
}
}
return &NoopMailService{}
}
func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
tpl, err := loadTemplate(tplNamePasswordReset)
if err != nil {
return nil, err
}
var rendered bytes.Buffer
if err := tpl.Execute(&rendered, data); err != nil {
return nil, err
}
return &rendered, nil
}
func getImportNotificationTemplate(data ImportNotificationTplData) (*bytes.Buffer, error) {
tpl, err := loadTemplate(tplNameImportNotification)
if err != nil {
return nil, err
}
var rendered bytes.Buffer
if err := tpl.Execute(&rendered, data); err != nil {
return nil, err
}
return &rendered, nil
}
func loadTemplate(tplName string) (*template.Template, error) {
tplFile, err := pkger.Open(fmt.Sprintf("%s/%s.tpl.html", tplPath, tplName))
if err != nil {
return nil, err
}
defer tplFile.Close()
tplData, err := ioutil.ReadAll(tplFile)
if err != nil {
return nil, err
}
return template.New(tplName).Parse(string(tplData))
}

View File

@ -0,0 +1,92 @@
package mail
import (
"bytes"
"encoding/json"
"errors"
"fmt"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"net/http"
"time"
)
type MailWhaleMailService struct {
publicUrl string
config *conf.MailwhaleMailConfig
httpClient *http.Client
}
type MailWhaleSendRequest struct {
To []string `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"`
Html string `json:"html"`
TemplateId string `json:"template_id"`
TemplateVars map[string]string `json:"template_vars"`
}
func NewMailWhaleService(config *conf.MailwhaleMailConfig, publicUrl string) *MailWhaleMailService {
return &MailWhaleMailService{
publicUrl: publicUrl,
config: config,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func (s *MailWhaleMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
if err != nil {
return err
}
return s.send(recipient.Email, subjectPasswordReset, template.String(), true)
}
func (s *MailWhaleMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
template, err := getImportNotificationTemplate(ImportNotificationTplData{
PublicUrl: s.publicUrl,
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
NumHeartbeats: numHeartbeats,
})
if err != nil {
return err
}
return s.send(recipient.Email, subjectImportNotification, template.String(), true)
}
func (s *MailWhaleMailService) send(to, subject, body string, isHtml bool) error {
if to == "" {
return errors.New("not sending mail as recipient mail address seems to be invalid")
}
sendRequest := &MailWhaleSendRequest{
To: []string{to},
Subject: subject,
}
if isHtml {
sendRequest.Html = body
} else {
sendRequest.Text = body
}
payload, _ := json.Marshal(sendRequest)
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/mail", s.config.Url), bytes.NewBuffer(payload))
if err != nil {
return err
}
req.SetBasicAuth(s.config.ClientId, s.config.ClientSecret)
req.Header.Set("Content-Type", "application/json")
res, err := s.httpClient.Do(req)
if err != nil {
return err
}
if res.StatusCode >= 400 {
return errors.New(fmt.Sprintf("got status %d from mailwhale", res.StatusCode))
}
return nil
}

19
services/mail/noop.go Normal file
View File

@ -0,0 +1,19 @@
package mail
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/models"
"time"
)
type NoopMailService struct{}
func (n *NoopMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to %s", recipient.ID)
return nil
}
func (n *NoopMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
logbuch.Info("noop mail service doing nothing instead of sending import notification mail to %s", recipient.ID)
return nil
}

117
services/mail/smtp.go Normal file
View File

@ -0,0 +1,117 @@
package mail
import (
"errors"
"fmt"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"io"
"time"
)
type SMTPMailService struct {
publicUrl string
config *conf.SMTPMailConfig
auth sasl.Client
}
func NewSMTPMailService(config *conf.SMTPMailConfig, publicUrl string) *SMTPMailService {
return &SMTPMailService{
publicUrl: publicUrl,
config: config,
auth: sasl.NewPlainClient(
"",
config.Username,
config.Password,
),
}
}
func (s *SMTPMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
if err != nil {
return err
}
mail := &models.Mail{
From: models.MailAddress(s.config.Sender),
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
Subject: subjectPasswordReset,
}
mail.WithHTML(template.String())
return s.send(s.config.ConnStr(), s.config.TLS, s.auth, mail.From.Raw(), mail.To.RawStrings(), mail.Reader())
}
func (s *SMTPMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
template, err := getImportNotificationTemplate(ImportNotificationTplData{
PublicUrl: s.publicUrl,
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
NumHeartbeats: numHeartbeats,
})
if err != nil {
return err
}
mail := &models.Mail{
From: models.MailAddress(s.config.Sender),
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
Subject: subjectImportNotification,
}
mail.WithHTML(template.String())
return s.send(s.config.ConnStr(), s.config.TLS, s.auth, mail.From.Raw(), mail.To.RawStrings(), mail.Reader())
}
func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string, to []string, r io.Reader) error {
dial := smtp.Dial
if tls {
dial = func(addr string) (*smtp.Client, error) {
return smtp.DialTLS(addr, nil)
}
}
c, err := dial(addr)
if err != nil {
return err
}
defer c.Close()
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(nil); err != nil {
return err
}
}
if a != nil {
if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err = c.Mail(from, nil); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}

1
services/mail/utils.go Normal file
View File

@ -0,0 +1 @@
package mail

View File

@ -64,6 +64,8 @@ type ISummaryService interface {
type IUserService interface {
GetUserById(string) (*models.User, error)
GetUserByKey(string) (*models.User, error)
GetUserByEmail(string) (*models.User, error)
GetUserByResetToken(string) (*models.User, error)
GetAll() ([]*models.User, error)
GetActive() ([]*models.User, error)
Count() (int64, error)
@ -73,5 +75,11 @@ type IUserService interface {
ResetApiKey(*models.User) (*models.User, error)
SetWakatimeApiKey(*models.User, string) (*models.User, error)
MigrateMd5Password(*models.User, *models.Login) (*models.User, error)
GenerateResetToken(*models.User) (*models.User, error)
FlushCache()
}
type IMailService interface {
SendPasswordReset(*models.User, string) error
SendImportNotification(*models.User, time.Duration, int) error
}

View File

@ -52,6 +52,14 @@ func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
return u, nil
}
func (srv *UserService) GetUserByEmail(email string) (*models.User, error) {
return srv.repository.GetByEmail(email)
}
func (srv *UserService) GetUserByResetToken(resetToken string) (*models.User, error) {
return srv.repository.GetByResetToken(resetToken)
}
func (srv *UserService) GetAll() ([]*models.User, error) {
return srv.repository.GetAll()
}
@ -110,6 +118,10 @@ func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Logi
return srv.repository.UpdateField(user, "password", user.Password)
}
func (srv *UserService) GenerateResetToken(user *models.User) (*models.User, error) {
return srv.repository.UpdateField(user, "reset_token", uuid.NewV4())
}
func (srv *UserService) Delete(user *models.User) error {
srv.cache.Flush()
return srv.repository.Delete(user)

View File

@ -371,21 +371,6 @@ function copyApiKey(event) {
event.stopPropagation()
}
// https://koddsson.com/posts/emoji-favicon/
const favicon = document.querySelector('link[rel=icon]')
if (favicon) {
const emoji = favicon.getAttribute('data-emoji')
if (emoji) {
const canvas = document.createElement('canvas')
canvas.height = 64
canvas.width = 64
const ctx = canvas.getContext('2d')
ctx.font = '64px serif'
ctx.fillText(emoji, 0, 64)
favicon.href = canvas.toDataURL()
}
}
// Click outside
window.addEventListener('click', function (event) {
if (event.target.classList.contains('popup')) {

View File

@ -1 +1 @@
1.25.4
1.26.1

View File

@ -2,6 +2,10 @@
<title>Wakapi Coding Statistics</title>
<base href="{{ getBasePath }}/">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<meta property="og:title" content="Wakapi - Coding Statistics" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Wakapi is an open-source tool that helps you keep track of the time you have spent coding on different projects in different programming languages and more. Ideal for statistics freaks and anyone else.">
<meta property="og:image" content="assets/images/screenshot.png" />
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">

View File

@ -46,6 +46,10 @@
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">📡 Host it
</button>
</a>
<a href="https://liberapay.com/muety" target="_blank" rel="noopener noreferrer">
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">🙏 Support it
</button>
</a>
<a href="https://github.com/muety/wakapi" target="_blank" rel="noopener noreferrer">
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">
<img alt="GitHub Icon" src="assets/images/ghicon.svg" width="22px">

View File

@ -7,8 +7,12 @@
{{ template "header.tpl.html" . }}
<div class="flex items-center justify-center">
<h1 class="font-semibold text-xl text-white m-0 border-b-4 border-green-700">Login</h1>
<div class="w-full flex justify-center">
<div class="flex items-center justify-between max-w-lg flex-grow">
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">&larr; Go back</a></div>
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Login</h1></div>
<div></div>
</div>
</div>
{{ template "alerts.tpl.html" . }}
@ -28,11 +32,16 @@
type="password" id="password"
name="password" placeholder="******" minlength="6" required>
</div>
<div class="flex justify-between">
<a href="signup">
<button type="button" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Sign up</button>
<div class="flex justify-between items-center">
<a href="reset-password" class="text-gray-500 text-sm">
Forgot password?
</a>
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Log in</button>
<div class="flex space-x-2">
<a href="signup">
<button type="button" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Sign up</button>
</a>
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Log in</button>
</div>
</div>
</form>
</div>

View File

@ -0,0 +1,152 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Wakapi Data Import Finished</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #047857 !important;
}
.btn-primary a:hover {
background-color: #047857 !important;
border-color: #047857 !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 18px; font-weight: 500; margin: 0; Margin-bottom: 15px;">Data import finished</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have requested to import data from WakaTime to Wakapi. The import has now finished after {{ .Duration }} ({{ .NumHeartbeats }} new heartbeats imported).<br><br>You should be able to see the newly imported coding statistics in Wakapi.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #2F855A; border-radius: 5px; text-align: center;"> <a href="{{ .PublicUrl }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #2F855A; border: solid 1px #2F855A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #2F855A;">Go to dashboard</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,153 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Wakapi Reset Password</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #047857 !important;
}
.btn-primary a:hover {
background-color: #047857 !important;
border-color: #047857 !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 18px; font-weight: 500; margin: 0; Margin-bottom: 15px;">Password Reset</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have requested to reset your Wakapi password. Please click the following link to proceed.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #2F855A; border-radius: 5px; text-align: center;"> <a href="{{ .ResetLink }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #2F855A; border: solid 1px #2F855A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #2F855A;">Reset Password</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you did not request a password change, please just ignore this mail.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head.tpl.html" . }}
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
{{ template "header.tpl.html" . }}
<div class="w-full flex justify-center">
<div class="flex items-center justify-between max-w-lg flex-grow">
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">&larr; Go back</a></div>
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Reset Password</h1></div>
<div></div>
</div>
</div>
{{ template "alerts.tpl.html" . }}
<main class="mt-10 flex-grow flex justify-center w-full">
<div class="flex-grow max-w-lg mt-10">
<form action="reset-password" method="post">
<p class="text-sm text-white mb-8">If you forgot your password, enter the e-mail address you associated with Wakapi to reset it and choose a new one. You will receive an e-mail afterwards. Make sure to check your spam folder if it does not arrive after a few minutes.</p>
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="email">E-Mail</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="email" id="email"
name="email" placeholder="Enter your e-mail address" minlength="1" required autofocus>
</div>
<div class="flex justify-end items-center">
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Reset</button>
</div>
</form>
</div>
</main>
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head.tpl.html" . }}
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
{{ template "header.tpl.html" . }}
<div class="flex items-center justify-center">
<h1 class="font-semibold text-xl text-white m-0 border-b-4 border-green-700">Choose a new password</h1>
</div>
{{ template "alerts.tpl.html" . }}
<main class="mt-10 flex-grow flex justify-center w-full">
<div class="flex-grow max-w-lg mt-10">
<form action="set-password" method="post">
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="password" id="password"
name="password" placeholder="Choose a password" minlength="6" required>
</div>
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="password_repeat">And again ...</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
type="password" id="password_repeat"
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
</div>
<div class="flex justify-end items-center">
<input type="hidden" name="token" value="{{ .Token }}">
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Save</button>
</div>
</form>
</div>
</main>
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
</body>
</html>

View File

@ -23,7 +23,7 @@
<div class="w-full flex justify-center">
<div class="flex items-center justify-between max-w-2xl flex-grow">
<div><a href="" class="text-gray-500 text-sm">&larr; Go back</a></div>
<div><a href="/" class="text-gray-500 text-sm cursor-pointer">&larr; Go back</a></div>
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
</div>

View File

@ -3,10 +3,13 @@
{{ template "head.tpl.html" . }}
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
{{ template "header.tpl.html" . }}
<div class="w-full flex justify-center">
<div class="flex items-center justify-between max-w-4xl flex-grow">
<div><a href="" class="text-gray-500 text-sm">&larr; Go back</a></div>
<div class="flex items-center justify-between max-w-lg flex-grow">
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">&larr; Go back</a></div>
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Sign Up</h1></div>
<div></div>
</div>

View File

@ -92,7 +92,7 @@
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col" id="project-container" style="height: 300px">
<div class="flex justify-between">
<div class="w-1/4 flex-1"></div>
<span class="font-semibold w-1/2 text-center flex-1">Projects</span>
<span class="font-semibold w-1/2 text-center flex-1 whitespace-no-wrap">Projects</span>
<div class="flex justify-end flex-1 text-xs items-center">
<label for="project-top-picker" class="mr-1">Show:&nbsp;</label>
<input type="number" min="1" id="project-top-picker" data-entity="0" class="w-1/4 top-picker bg-gray-800 rounded-md text-center" value="10">
@ -108,7 +108,7 @@
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col" id="os-container" style="height: 300px">
<div class="flex justify-between">
<div class="w-1/4 flex-1"></div>
<span class="font-semibold w-1/2 text-center flex-1">Operating Systems</span>
<span class="font-semibold w-1/2 text-center flex-1 whitespace-no-wrap">Operating Systems</span>
<div class="flex justify-end flex-1 text-xs items-center">
<label for="os-top-picker" class="mr-1">Show:&nbsp;</label>
<input type="number" min="1" id="os-top-picker" data-entity="1" class="w-1/4 top-picker bg-gray-800 rounded-md text-center" value="10">
@ -124,7 +124,7 @@
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col relative" id="language-container" style="height: 300px">
<div class="flex justify-between">
<div class="w-1/4 flex-1"></div>
<span class="font-semibold w-1/2 text-center flex-1">Languages</span>
<span class="font-semibold w-1/2 text-center flex-1 whitespace-no-wrap">Languages</span>
<div class="flex justify-end flex-1 text-xs items-center">
<label for="language-top-picker" class="mr-1">Show:&nbsp;</label>
<input type="number" min="1" id="language-top-picker" data-entity="3" class="w-1/4 top-picker bg-gray-800 rounded-md text-center" value="10">
@ -140,7 +140,7 @@
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col" id="editor-container" style="height: 300px">
<div class="flex justify-between">
<div class="w-1/4 flex-1"></div>
<span class="font-semibold w-1/2 text-center flex-1">Editors</span>
<span class="font-semibold w-1/2 text-center flex-1 whitespace-no-wrap">Editors</span>
<div class="flex justify-end flex-1 text-xs items-center">
<label for="editor-top-picker" class="mr-1">Show:&nbsp;</label>
<input type="number" min="1" id="editor-top-picker" data-entity="2" class="w-1/4 top-picker bg-gray-800 rounded-md text-center" value="10">
@ -156,7 +156,7 @@
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col" id="machine-container" style="height: 300px">
<div class="flex justify-between">
<div class="w-1/4 flex-1"></div>
<span class="font-semibold w-1/2 text-center flex-1">Machines</span>
<span class="font-semibold w-1/2 text-center flex-1 whitespace-no-wrap">Machines</span>
<div class="flex justify-end flex-1 text-xs items-center">
<label for="machine-top-picker" class="mr-1">Show:&nbsp;</label>
<input type="number" min="1" id="machine-top-picker" data-entity="4" class="w-1/4 top-picker bg-gray-800 rounded-md text-center" value="10">
@ -209,8 +209,10 @@
{{ template "foot.tpl.html" . }}
<script type="text/javascript">
document.querySelector('#api-key-instruction').innerHTML = document.querySelector('#api-key-container').value
<script>
document.addEventListener('load', function() {
document.getElementById('api-key-instruction').innerHTML = document.getElementById('api-key-container').value
})
</script>
<script>