diff --git a/migrations/sqlite3/3_user_creation_date.sql b/migrations/sqlite3/3_user_creation_date.sql new file mode 100644 index 0000000..8450d8e --- /dev/null +++ b/migrations/sqlite3/3_user_creation_date.sql @@ -0,0 +1,20 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +-- SQLite does not allow altering a table to add a new column with default of CURRENT_TIMESTAMP +-- See https://www.sqlite.org/lang_altertable.html + +alter table users + add `created_at` timestamp default '2020-01-01T00:00:00.000' not null; + +alter table users + add `last_logged_in_at` timestamp default '2020-01-01T00:00:00.000' not null; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +alter table users + drop column `created_at`; + +alter table users + drop column `last_logged_in_at`; \ No newline at end of file diff --git a/models/heartbeat.go b/models/heartbeat.go index 61fad35..62893a6 100644 --- a/models/heartbeat.go +++ b/models/heartbeat.go @@ -1,36 +1,31 @@ package models import ( - "database/sql/driver" - "errors" - "fmt" "regexp" - "strconv" - "strings" "time" ) -type HeartbeatReqTime time.Time +type CustomTime time.Time type Heartbeat struct { - ID uint `gorm:"primary_key"` - User *User `json:"-" gorm:"not null"` - 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"` - OperatingSystem string `json:"operating_system"` - Time HeartbeatReqTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user"` + ID uint `gorm:"primary_key"` + User *User `json:"-" gorm:"not null"` + 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"` + OperatingSystem string `json:"operating_system"` + Time CustomTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user"` languageRegex *regexp.Regexp } func (h *Heartbeat) Valid() bool { - return h.User != nil && h.UserID != "" && h.Time != HeartbeatReqTime(time.Time{}) + return h.User != nil && h.UserID != "" && h.Time != CustomTime(time.Time{}) } func (h *Heartbeat) Augment(customLangs map[string]string) { @@ -49,47 +44,3 @@ func (h *Heartbeat) Augment(customLangs map[string]string) { h.Language, _ = customLangs[ending] } } - -func (j *HeartbeatReqTime) UnmarshalJSON(b []byte) error { - s := strings.Split(strings.Trim(string(b), "\""), ".")[0] - i, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return err - } - t := time.Unix(i, 0) - *j = HeartbeatReqTime(t) - return nil -} - -func (j *HeartbeatReqTime) Scan(value interface{}) error { - switch value.(type) { - case string: - t, err := time.Parse("2006-01-02 15:04:05-07:00", value.(string)) - if err != nil { - return errors.New(fmt.Sprintf("unsupported date time format: %s", value)) - } - *j = HeartbeatReqTime(t) - case int64: - *j = HeartbeatReqTime(time.Unix(value.(int64), 0)) - break - case time.Time: - *j = HeartbeatReqTime(value.(time.Time)) - break - default: - return errors.New(fmt.Sprintf("unsupported type: %T", value)) - } - return nil -} - -func (j HeartbeatReqTime) Value() (driver.Value, error) { - return time.Time(j), nil -} - -func (j HeartbeatReqTime) String() string { - t := time.Time(j) - return t.Format("2006-01-02 15:04:05") -} - -func (j HeartbeatReqTime) Time() time.Time { - return time.Time(j) -} diff --git a/models/shared.go b/models/shared.go index bc03026..01d820f 100644 --- a/models/shared.go +++ b/models/shared.go @@ -1,6 +1,14 @@ package models -import "github.com/jinzhu/gorm" +import ( + "database/sql/driver" + "errors" + "fmt" + "github.com/jinzhu/gorm" + "strconv" + "strings" + "time" +) const ( UserKey = "user" @@ -14,3 +22,47 @@ type KeyStringValue struct { Key string `gorm:"primary_key"` Value string `gorm:"type:text"` } + +func (j *CustomTime) UnmarshalJSON(b []byte) error { + s := strings.Split(strings.Trim(string(b), "\""), ".")[0] + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t := time.Unix(i, 0) + *j = CustomTime(t) + return nil +} + +func (j *CustomTime) Scan(value interface{}) error { + switch value.(type) { + case string: + t, err := time.Parse("2006-01-02 15:04:05-07:00", value.(string)) + if err != nil { + return errors.New(fmt.Sprintf("unsupported date time format: %s", value)) + } + *j = CustomTime(t) + case int64: + *j = CustomTime(time.Unix(value.(int64), 0)) + break + case time.Time: + *j = CustomTime(value.(time.Time)) + break + default: + return errors.New(fmt.Sprintf("unsupported type: %T", value)) + } + return nil +} + +func (j CustomTime) Value() (driver.Value, error) { + return time.Time(j), nil +} + +func (j CustomTime) String() string { + t := time.Time(j) + return t.Format("2006-01-02 15:04:05") +} + +func (j CustomTime) Time() time.Time { + return time.Time(j) +} diff --git a/models/user.go b/models/user.go index 208d5e6..ca4a09e 100644 --- a/models/user.go +++ b/models/user.go @@ -1,9 +1,11 @@ package models type User struct { - ID string `json:"id" gorm:"primary_key"` - ApiKey string `json:"api_key" gorm:"unique"` - Password string `json:"-"` + ID string `json:"id" gorm:"primary_key"` + ApiKey string `json:"api_key" gorm:"unique"` + Password string `json:"-"` + CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"` + LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"` } type Login struct { diff --git a/routes/public.go b/routes/public.go index 44ed869..4b2cf47 100644 --- a/routes/public.go +++ b/routes/public.go @@ -9,6 +9,7 @@ import ( "github.com/muety/wakapi/utils" "net/http" "net/url" + "time" ) type IndexHandler struct { @@ -106,6 +107,9 @@ func (h *IndexHandler) Login(w http.ResponseWriter, r *http.Request) { return } + user.LastLoggedInAt = models.CustomTime(time.Now()) + h.userSrvc.Update(user) + cookie := &http.Cookie{ Name: models.AuthCookieKey, Value: encoded, diff --git a/services/user.go b/services/user.go index 81768a8..19802c8 100644 --- a/services/user.go +++ b/services/user.go @@ -69,6 +69,19 @@ func (srv *UserService) CreateOrGet(signup *models.Signup) (*models.User, bool, return u, false, nil } +func (srv *UserService) Update(user *models.User) (*models.User, error) { + result := srv.Db.Model(&models.User{}).Updates(user) + if err := result.Error; err != nil { + return nil, err + } + + if result.RowsAffected != 1 { + return nil, errors.New("nothing updated") + } + + return user, nil +} + func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) { user.Password = login.Password if err := utils.HashPassword(user, srv.Config.PasswordSalt); err != nil { diff --git a/version.txt b/version.txt index 2eda823..308b6fa 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.1 \ No newline at end of file +1.6.2 \ No newline at end of file