From b39bc4f79a87c7d2a47e57705a99bb8fadcdde5d Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Wed, 23 Oct 2024 10:02:11 +0200 Subject: [PATCH] refactor: save dates as unix timestamps in database --- backend/internal/dto/dto_mapper.go | 13 +++++ backend/internal/job/db_cleanup.go | 9 ++-- backend/internal/model/base.go | 4 +- backend/internal/model/oidc.go | 4 +- backend/internal/model/types/date_time.go | 47 +++++++++++++++++++ backend/internal/model/user.go | 4 +- backend/internal/service/oidc_service.go | 5 +- backend/internal/service/test_service.go | 5 +- backend/internal/service/user_service.go | 5 +- backend/internal/utils/time_util.go | 8 ---- .../20241023072742_unix-timestamps.down.sql | 28 +++++++++++ .../20241023072742_unix-timestamps.up.sql | 27 +++++++++++ 12 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 backend/internal/model/types/date_time.go delete mode 100644 backend/internal/utils/time_util.go create mode 100644 backend/migrations/20241023072742_unix-timestamps.down.sql create mode 100644 backend/migrations/20241023072742_unix-timestamps.up.sql diff --git a/backend/internal/dto/dto_mapper.go b/backend/internal/dto/dto_mapper.go index f8718f7..0456d84 100644 --- a/backend/internal/dto/dto_mapper.go +++ b/backend/internal/dto/dto_mapper.go @@ -2,7 +2,9 @@ package dto import ( "errors" + "github.com/stonith404/pocket-id/backend/internal/model/types" "reflect" + "time" ) // MapStructList maps a list of source structs to a list of destination structs @@ -95,7 +97,18 @@ func mapStructInternal(sourceVal reflect.Value, destVal reflect.Value) error { if err := mapStructInternal(sourceField, destField); err != nil { return err } + } else { + // Type switch for specific type conversions + switch sourceField.Interface().(type) { + case datatype.DateTime: + // Convert datatype.DateTime to time.Time + if sourceField.Type() == reflect.TypeOf(datatype.DateTime{}) && destField.Type() == reflect.TypeOf(time.Time{}) { + dateValue := sourceField.Interface().(datatype.DateTime) + destField.Set(reflect.ValueOf(dateValue.ToTime())) + } + } } + } } diff --git a/backend/internal/job/db_cleanup.go b/backend/internal/job/db_cleanup.go index da2dc27..52bdc85 100644 --- a/backend/internal/job/db_cleanup.go +++ b/backend/internal/job/db_cleanup.go @@ -4,7 +4,6 @@ import ( "github.com/go-co-op/gocron/v2" "github.com/google/uuid" "github.com/stonith404/pocket-id/backend/internal/model" - "github.com/stonith404/pocket-id/backend/internal/utils" "gorm.io/gorm" "log" "time" @@ -30,22 +29,22 @@ type Jobs struct { // ClearWebauthnSessions deletes WebAuthn sessions that have expired func (j *Jobs) clearWebauthnSessions() error { - return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error + return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", time.Now().Unix()).Error } // ClearOneTimeAccessTokens deletes one-time access tokens that have expired func (j *Jobs) clearOneTimeAccessTokens() error { - return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error + return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", time.Now().Unix()).Error } // ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired func (j *Jobs) clearOidcAuthorizationCodes() error { - return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error + return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", time.Now().Unix()).Error } // ClearAuditLogs deletes audit logs older than 90 days func (j *Jobs) clearAuditLogs() error { - return j.db.Delete(&model.AuditLog{}, "created_at < ?", utils.FormatDateForDb(time.Now().AddDate(0, 0, -90))).Error + return j.db.Delete(&model.AuditLog{}, "created_at < ?", time.Now().AddDate(0, 0, -90).Unix()).Error } func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) { diff --git a/backend/internal/model/base.go b/backend/internal/model/base.go index 68f0da2..0392633 100644 --- a/backend/internal/model/base.go +++ b/backend/internal/model/base.go @@ -2,6 +2,7 @@ package model import ( "github.com/google/uuid" + model "github.com/stonith404/pocket-id/backend/internal/model/types" "gorm.io/gorm" "time" ) @@ -9,12 +10,13 @@ import ( // Base contains common columns for all tables. type Base struct { ID string `gorm:"primaryKey;not null"` - CreatedAt time.Time + CreatedAt model.DateTime } func (b *Base) BeforeCreate(_ *gorm.DB) (err error) { if b.ID == "" { b.ID = uuid.New().String() } + b.CreatedAt = model.DateTime(time.Now()) return } diff --git a/backend/internal/model/oidc.go b/backend/internal/model/oidc.go index 4d914a8..7b0dacc 100644 --- a/backend/internal/model/oidc.go +++ b/backend/internal/model/oidc.go @@ -4,8 +4,8 @@ import ( "database/sql/driver" "encoding/json" "errors" + datatype "github.com/stonith404/pocket-id/backend/internal/model/types" "gorm.io/gorm" - "time" ) type UserAuthorizedOidcClient struct { @@ -23,7 +23,7 @@ type OidcAuthorizationCode struct { Code string Scope string Nonce string - ExpiresAt time.Time + ExpiresAt datatype.DateTime UserID string User User diff --git a/backend/internal/model/types/date_time.go b/backend/internal/model/types/date_time.go new file mode 100644 index 0000000..17c5761 --- /dev/null +++ b/backend/internal/model/types/date_time.go @@ -0,0 +1,47 @@ +package datatype + +import ( + "database/sql/driver" + "time" +) + +// DateTime custom type for time.Time to store date as unix timestamp in the database +type DateTime time.Time + +func (date *DateTime) Scan(value interface{}) (err error) { + *date = DateTime(value.(time.Time)) + return +} + +func (date DateTime) Value() (driver.Value, error) { + return time.Time(date).Unix(), nil +} + +func (date DateTime) UTC() time.Time { + return time.Time(date).UTC() +} + +func (date DateTime) ToTime() time.Time { + return time.Time(date) +} + +// GormDataType gorm common data type +func (date DateTime) GormDataType() string { + return "date" +} + +func (date DateTime) GobEncode() ([]byte, error) { + return time.Time(date).GobEncode() +} + +func (date *DateTime) GobDecode(b []byte) error { + return (*time.Time)(date).GobDecode(b) +} + +func (date DateTime) MarshalJSON() ([]byte, error) { + return time.Time(date).MarshalJSON() +} + +func (date *DateTime) UnmarshalJSON(b []byte) error { + return (*time.Time)(date).UnmarshalJSON(b) +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 7a62ea5..8cb6f0b 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -3,7 +3,7 @@ package model import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" - "time" + "github.com/stonith404/pocket-id/backend/internal/model/types" ) type User struct { @@ -61,7 +61,7 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential type OneTimeAccessToken struct { Base Token string - ExpiresAt time.Time + ExpiresAt datatype.DateTime UserID string User User diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index e004bb7..64fbb5b 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -6,6 +6,7 @@ import ( "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" + datatype "github.com/stonith404/pocket-id/backend/internal/model/types" "github.com/stonith404/pocket-id/backend/internal/utils" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -115,7 +116,7 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret strin return "", "", common.ErrOidcInvalidAuthorizationCode } - if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.Before(time.Now()) { + if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) { return "", "", common.ErrOidcInvalidAuthorizationCode } @@ -350,7 +351,7 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc } oidcAuthorizationCode := model.OidcAuthorizationCode{ - ExpiresAt: time.Now().Add(15 * time.Minute), + ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)), Code: randomString, ClientID: clientID, UserID: userID, diff --git a/backend/internal/service/test_service.go b/backend/internal/service/test_service.go index a57da12..5fba0c3 100644 --- a/backend/internal/service/test_service.go +++ b/backend/internal/service/test_service.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "fmt" "github.com/fxamacker/cbor/v2" + "github.com/stonith404/pocket-id/backend/internal/model/types" "log" "os" "time" @@ -111,7 +112,7 @@ func (s *TestService) SeedDatabase() error { Code: "auth-code", Scope: "openid profile", Nonce: "nonce", - ExpiresAt: time.Now().Add(1 * time.Hour), + ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), UserID: users[0].ID, ClientID: oidcClients[0].ID, } @@ -121,7 +122,7 @@ func (s *TestService) SeedDatabase() error { accessToken := model.OneTimeAccessToken{ Token: "one-time-token", - ExpiresAt: time.Now().Add(1 * time.Hour), + ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), UserID: users[0].ID, } if err := tx.Create(&accessToken).Error; err != nil { diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index bb3c856..0c94a3b 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -5,6 +5,7 @@ import ( "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" + "github.com/stonith404/pocket-id/backend/internal/model/types" "github.com/stonith404/pocket-id/backend/internal/utils" "gorm.io/gorm" "time" @@ -95,7 +96,7 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim oneTimeAccessToken := model.OneTimeAccessToken{ UserID: userID, - ExpiresAt: expiresAt, + ExpiresAt: datatype.DateTime(expiresAt), Token: randomString, } @@ -108,7 +109,7 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, string, error) { var oneTimeAccessToken model.OneTimeAccessToken - if err := s.db.Where("token = ? AND expires_at > ?", token, utils.FormatDateForDb(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil { + if err := s.db.Where("token = ? AND expires_at > ?", token, time.Now().Unix()).Preload("User").First(&oneTimeAccessToken).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return model.User{}, "", common.ErrTokenInvalidOrExpired } diff --git a/backend/internal/utils/time_util.go b/backend/internal/utils/time_util.go deleted file mode 100644 index 18d85cd..0000000 --- a/backend/internal/utils/time_util.go +++ /dev/null @@ -1,8 +0,0 @@ -package utils - -import "time" - -func FormatDateForDb(time time.Time) string { - const layout = "2006-01-02 15:04:05.000-07:00" - return time.Format(layout) -} diff --git a/backend/migrations/20241023072742_unix-timestamps.down.sql b/backend/migrations/20241023072742_unix-timestamps.down.sql new file mode 100644 index 0000000..27befe1 --- /dev/null +++ b/backend/migrations/20241023072742_unix-timestamps.down.sql @@ -0,0 +1,28 @@ +-- Convert the Unix timestamps back to DATETIME format + +UPDATE user_groups +SET created_at = datetime(created_at, 'unixepoch'); + +UPDATE users +SET created_at = datetime(created_at, 'unixepoch'); + +UPDATE audit_logs +SET created_at = datetime(created_at, 'unixepoch'); + +UPDATE oidc_authorization_codes +SET created_at = datetime(created_at, 'unixepoch'), + expires_at = datetime(expires_at, 'unixepoch'); + +UPDATE oidc_clients +SET created_at = datetime(created_at, 'unixepoch'); + +UPDATE one_time_access_tokens +SET created_at = datetime(created_at, 'unixepoch'), + expires_at = datetime(expires_at, 'unixepoch'); + +UPDATE webauthn_credentials +SET created_at = datetime(created_at, 'unixepoch'); + +UPDATE webauthn_sessions +SET created_at = datetime(created_at, 'unixepoch'), + expires_at = datetime(expires_at, 'unixepoch'); \ No newline at end of file diff --git a/backend/migrations/20241023072742_unix-timestamps.up.sql b/backend/migrations/20241023072742_unix-timestamps.up.sql new file mode 100644 index 0000000..de1acb0 --- /dev/null +++ b/backend/migrations/20241023072742_unix-timestamps.up.sql @@ -0,0 +1,27 @@ +-- Convert the DATETIME fields to Unix timestamps (in seconds) +UPDATE user_groups +SET created_at = strftime('%s', created_at); + +UPDATE users +SET created_at = strftime('%s', created_at); + +UPDATE audit_logs +SET created_at = strftime('%s', created_at); + +UPDATE oidc_authorization_codes +SET created_at = strftime('%s', created_at), + expires_at = strftime('%s', expires_at); + +UPDATE oidc_clients +SET created_at = strftime('%s', created_at); + +UPDATE one_time_access_tokens +SET created_at = strftime('%s', created_at), + expires_at = strftime('%s', expires_at); + +UPDATE webauthn_credentials +SET created_at = strftime('%s', created_at); + +UPDATE webauthn_sessions +SET created_at = strftime('%s', created_at), + expires_at = strftime('%s', expires_at); \ No newline at end of file