diff --git a/backend/internal/dto/dto_mapper.go b/backend/internal/dto/dto_mapper.go index f8718f7b..0456d844 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 da2dc271..52bdc85b 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 68f0da24..0392633c 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 4d914a8a..7b0daccb 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 00000000..17c57615 --- /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 7a62ea53..8cb6f0b9 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 e004bb77..64fbb5b3 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 a57da127..5fba0c30 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 bb3c8569..0c94a3b1 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 18d85cde..00000000 --- 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 00000000..27befe12 --- /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 00000000..de1acb04 --- /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