Skip to content

Commit

Permalink
chore: introduce user email addresses (resolve #132)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Feb 21, 2021
1 parent 81d3251 commit 017530a
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 422 deletions.
3 changes: 2 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ func Test_mysqlConnectionString(t *testing.T) {
Password: "test_password",
Name: "test_name",
Dialect: "mysql",
Charset: "utf8mb4",
MaxConn: 10,
}

assert.Equal(t, fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
c.User,
c.Password,
c.Host,
Expand Down
787 changes: 396 additions & 391 deletions coverage/coverage.out

Large diffs are not rendered by default.

48 changes: 25 additions & 23 deletions models/heartbeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,39 @@ import (
"time"
)

var languageRegex *regexp.Regexp

func init() {
languageRegex = regexp.MustCompile(`^.+\.(.+)$`)
}

type Heartbeat struct {
ID uint `gorm:"primary_key" hash:"ignore"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
Type string `json:"type"`
Category string `json:"category"`
Project string `json:"project"`
Branch string `json:"branch"`
Language string `json:"language" gorm:"index:idx_language"`
IsWrite bool `json:"is_write"`
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
Origin string `json:"-" hash:"ignore"`
OriginId string `json:"-" hash:"ignore"`
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number"` // https://gorm.io/docs/conventions.html#CreatedAt
languageRegex *regexp.Regexp `hash:"ignore"`
ID uint `gorm:"primary_key" hash:"ignore"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
Type string `json:"type"`
Category string `json:"category"`
Project string `json:"project"`
Branch string `json:"branch"`
Language string `json:"language" gorm:"index:idx_language"`
IsWrite bool `json:"is_write"`
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
Origin string `json:"-" hash:"ignore"`
OriginId string `json:"-" hash:"ignore"`
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number"` // https://gorm.io/docs/conventions.html#CreatedAt
}

func (h *Heartbeat) Valid() bool {
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
}

func (h *Heartbeat) Augment(languageMappings map[string]string) {
if h.languageRegex == nil {
h.languageRegex = regexp.MustCompile(`^.+\.(.+)$`)
}
groups := h.languageRegex.FindAllStringSubmatch(h.Entity, -1)
groups := languageRegex.FindAllStringSubmatch(h.Entity, -1)
if len(groups) == 0 || len(groups[0]) != 2 {
return
}
Expand Down
39 changes: 34 additions & 5 deletions models/user.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
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)
}

type User struct {
ID string `json:"id" gorm:"primary_key"`
ApiKey string `json:"api_key" gorm:"unique"`
Email string `json:"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"`
Expand All @@ -24,6 +39,7 @@ type Login struct {

type Signup struct {
Username string `schema:"username"`
Email string `schema:"email"`
Password string `schema:"password"`
PasswordRepeat string `schema:"password_repeat"`
}
Expand All @@ -34,6 +50,10 @@ type CredentialsReset struct {
PasswordRepeat string `schema:"password_repeat"`
}

type UserDataUpdate struct {
Email string `schema:"email"`
}

type TimeByUser struct {
User string
Time CustomTime
Expand All @@ -45,20 +65,29 @@ type CountByUser struct {
}

func (c *CredentialsReset) IsValid() bool {
return validatePassword(c.PasswordNew) &&
return ValidatePassword(c.PasswordNew) &&
c.PasswordNew == c.PasswordRepeat
}

func (s *Signup) IsValid() bool {
return validateUsername(s.Username) &&
validatePassword(s.Password) &&
return ValidateUsername(s.Username) &&
ValidateEmail(s.Email) &&
ValidatePassword(s.Password) &&
s.Password == s.PasswordRepeat
}

func validateUsername(username string) bool {
func (r *UserDataUpdate) IsValid() bool {
return ValidateEmail(r.Email)
}

func ValidateUsername(username string) bool {
return len(username) >= 1 && username != "current"
}

func validatePassword(password string) bool {
func ValidatePassword(password string) bool {
return len(password) >= 6
}

func ValidateEmail(email string) bool {
return email == "" || mailRegex.Match([]byte(email))
}
1 change: 1 addition & 0 deletions repositories/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
updateMap := map[string]interface{}{
"api_key": user.ApiKey,
"password": user.Password,
"email": user.Email,
"last_logged_in_at": user.LastLoggedInAt,
"share_data_max_days": user.ShareDataMaxDays,
"share_editors": user.ShareEditors,
Expand Down
3 changes: 3 additions & 0 deletions routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func loadTemplates() {
"toRunes": utils.ToRunes,
"entityTypes": models.SummaryTypes,
"typeName": typeName,
"isDev": func() bool {
return config.Get().IsDev()
},
"getBasePath": func() string {
return config.Get().Server.BasePath
},
Expand Down
30 changes: 30 additions & 0 deletions routes/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ func (h *SettingsHandler) dispatchAction(action string) action {
switch action {
case "change_password":
return h.actionChangePassword
case "update_user":
return h.actionUpdateUser
case "reset_apikey":
return h.actionResetApiKey
case "delete_alias":
Expand All @@ -141,6 +143,34 @@ func (h *SettingsHandler) dispatchAction(action string) action {
return nil
}

func (h *SettingsHandler) actionUpdateUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
if h.config.IsDev() {
loadTemplates()
}

user := r.Context().Value(models.UserKey).(*models.User)

var payload models.UserDataUpdate
if err := r.ParseForm(); err != nil {
return http.StatusBadRequest, "", "missing parameters"
}
if err := credentialsDecoder.Decode(&payload, r.PostForm); err != nil {
return http.StatusBadRequest, "", "missing parameters"
}

if !payload.IsValid() {
return http.StatusBadRequest, "", "invalid parameters"
}

user.Email = payload.Email

if _, err := h.userSrvc.Update(user); err != nil {
return http.StatusInternalServerError, "", conf.ErrInternalServerError
}

return http.StatusOK, "alias added successfully", ""
}

func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Request) (int, string, string) {
if h.config.IsDev() {
loadTemplates()
Expand Down
1 change: 1 addition & 0 deletions services/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (srv *UserService) Count() (int64, error) {
func (srv *UserService) CreateOrGet(signup *models.Signup, isAdmin bool) (*models.User, bool, error) {
u := &models.User{
ID: signup.Username,
Email: signup.Email,
ApiKey: uuid.NewV4().String(),
Password: signup.Password,
IsAdmin: isAdmin,
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.24.4
1.24.5
26 changes: 26 additions & 0 deletions views/settings.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@
<div class="flex flex-col flex-grow max-w-2xl mt-8">

<details class="my-8 pb-8 border-b border-gray-700">
<summary class="cursor-pointer">
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block"
id="email-heading">
Change E-Mail Address
</h2>
</summary>
<div class="w-full">
<form class="mt-10" action="" method="post">
<input type="hidden" name="action" value="update_user">
<div class="mb-8 flex justify-between items-center space-x-4">
<label class="inline-block text-sm text-gray-500" for="password_old">E-Mail Address</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 flex-grow py-1 px-3"
type="email" id="email"
name="email" placeholder="Enter your e-mail address"
value="{{ .User.Email }}">
<button type="submit"
class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
Save
</button>
</div>
<div class="text-gray-300 text-sm">E-Mail address is optional, but required for some features that you cannot use else. Also, if you do not add an e-mail address, you will not be able to reset your password in case you forget it.</div>
</form>
</div>
</details>

<details class="mb-8 pb-8 border-b border-gray-700">
<summary class="cursor-pointer">
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="password">
Change Password
Expand Down
9 changes: 8 additions & 1 deletion views/signup.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

{{ template "head.tpl.html" . }}

<body class="bg-gray-800 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-xl mx-auto justify-center">
<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>
Expand Down Expand Up @@ -37,6 +37,13 @@
type="text" id="username"
name="username" placeholder="Choose a username" minlength="1" required autofocus>
</div>
<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="Optionally add your e-mail address">
<div class="text-xs text-gray-500 mt-2 italic">E-Mail address is optional, but required for some features that you cannot use else. Also, if you do not add an e-mail address, you will not be able to reset your password in case you forget it.</div>
</div>
<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"
Expand Down

0 comments on commit 017530a

Please sign in to comment.