Merge branch '133-password-resets'

This commit is contained in:
Ferdinand Mütsch 2021-04-10 00:34:20 +02:00
commit 0df0168584
30 changed files with 1186 additions and 276 deletions

View File

@ -180,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"
}
@ -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

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)
@ -159,7 +162,7 @@ func main() {
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
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())
}

57
models/mail_address.go Normal file
View File

@ -0,0 +1,57 @@
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
}

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

@ -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.SendPasswordResetMail(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()

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

@ -0,0 +1,61 @@
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"
subjectPasswordReset = "Wakapi Password Reset"
)
type passwordResetLinkTplData struct {
ResetLink string
}
// Factory
func NewMailService() services.IMailService {
config := conf.Get()
if config.Mail.Enabled {
if config.Mail.Provider == conf.MailProviderMailWhale {
return NewMailWhaleService(config.Mail.MailWhale)
} else if config.Mail.Provider == conf.MailProviderSmtp {
return NewSMTPMailService(config.Mail.Smtp)
}
}
return &NoopMailService{}
}
func getPasswordResetTemplate(data passwordResetLinkTplData) (*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 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,78 @@
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 {
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) *MailWhaleMailService {
return &MailWhaleMailService{
config: config,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func (s *MailWhaleMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
template, err := getPasswordResetTemplate(passwordResetLinkTplData{ResetLink: resetLink})
if err != nil {
return err
}
return s.send(recipient.Email, subjectPasswordReset, template.String(), true)
}
func (s *MailWhaleMailService) send(to, subject, body string, isHtml bool) error {
if to == "" {
return errors.New("no recipient mail address set, cannot send password reset link")
}
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
}

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

@ -0,0 +1,13 @@
package mail
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/models"
)
type NoopMailService struct{}
func (n *NoopMailService) SendPasswordResetMail(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
}

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

@ -0,0 +1,100 @@
package mail
import (
"errors"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"io"
)
type SMTPMailService struct {
config *conf.SMTPMailConfig
auth sasl.Client
}
func NewSMTPMailService(config *conf.SMTPMailConfig) *SMTPMailService {
return &SMTPMailService{
config: config,
auth: sasl.NewPlainClient(
"",
config.Username,
config.Password,
),
}
}
func (s *SMTPMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
template, err := getPasswordResetTemplate(passwordResetLinkTplData{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) 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,10 @@ 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 {
SendPasswordResetMail(recipient *models.User, resetLink string) 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

@ -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,154 @@
<!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>Simple Transactional Email</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%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
<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 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">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>