This commit is contained in:
2025-08-22 19:19:16 -04:00
parent df567983c1
commit 93831d4fad
36 changed files with 22412 additions and 29 deletions

View File

@ -0,0 +1,47 @@
package domain
import (
"encoding/json"
"fmt"
"time"
)
// Duration is a wrapper around time.Duration that can unmarshal from both
// string duration formats (like "168h") and nanosecond integers
type Duration struct {
time.Duration
}
// UnmarshalJSON implements json.Unmarshaler interface
func (d *Duration) UnmarshalJSON(data []byte) error {
// Try to unmarshal as string first (e.g., "168h", "24h", "30m")
var str string
if err := json.Unmarshal(data, &str); err == nil {
duration, err := time.ParseDuration(str)
if err != nil {
return fmt.Errorf("invalid duration format: %s", str)
}
d.Duration = duration
return nil
}
// Try to unmarshal as integer (nanoseconds)
var ns int64
if err := json.Unmarshal(data, &ns); err == nil {
d.Duration = time.Duration(ns)
return nil
}
return fmt.Errorf("duration must be either a string (e.g., '168h') or integer nanoseconds")
}
// MarshalJSON implements json.Marshaler interface
func (d Duration) MarshalJSON() ([]byte, error) {
// Always marshal as nanoseconds for consistency
return json.Marshal(int64(d.Duration))
}
// String returns the string representation of the duration
func (d Duration) String() string {
return d.Duration.String()
}

View File

@ -44,8 +44,8 @@ type Application struct {
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user" db:"type"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500" db:"callback_url"`
HMACKey string `json:"hmac_key" validate:"required,min=1,max=255" db:"hmac_key"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1" db:"token_renewal_duration"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1" db:"max_token_duration"`
TokenRenewalDuration Duration `json:"token_renewal_duration" validate:"required,min=1" db:"token_renewal_duration"`
MaxTokenDuration Duration `json:"max_token_duration" validate:"required,min=1" db:"max_token_duration"`
Owner Owner `json:"owner" validate:"required"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
@ -157,8 +157,8 @@ type CreateApplicationRequest struct {
AppLink string `json:"app_link" validate:"required,url,max=500"`
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1"`
TokenRenewalDuration Duration `json:"token_renewal_duration" validate:"required,min=1"`
MaxTokenDuration Duration `json:"max_token_duration" validate:"required,min=1"`
Owner Owner `json:"owner" validate:"required"`
}
@ -168,8 +168,8 @@ type UpdateApplicationRequest struct {
Type *[]ApplicationType `json:"type,omitempty" validate:"omitempty,min=1,dive,oneof=static user"`
CallbackURL *string `json:"callback_url,omitempty" validate:"omitempty,url,max=500"`
HMACKey *string `json:"hmac_key,omitempty" validate:"omitempty,min=1,max=255"`
TokenRenewalDuration *time.Duration `json:"token_renewal_duration,omitempty" validate:"omitempty,min=1"`
MaxTokenDuration *time.Duration `json:"max_token_duration,omitempty" validate:"omitempty,min=1"`
TokenRenewalDuration *Duration `json:"token_renewal_duration,omitempty" validate:"omitempty,min=1"`
MaxTokenDuration *Duration `json:"max_token_duration,omitempty" validate:"omitempty,min=1"`
Owner *Owner `json:"owner,omitempty" validate:"omitempty"`
}

View File

@ -38,7 +38,7 @@ type TenantSettings struct {
OAuth2Settings *OAuth2Settings `json:"oauth2_settings,omitempty"`
// Session settings
SessionTimeout time.Duration `json:"session_timeout,omitempty"`
SessionTimeout Duration `json:"session_timeout,omitempty"`
MaxConcurrentSessions int `json:"max_concurrent_sessions,omitempty"`
// Security settings
@ -47,8 +47,8 @@ type TenantSettings struct {
PasswordPolicy *PasswordPolicy `json:"password_policy,omitempty"`
// Token settings
DefaultTokenDuration time.Duration `json:"default_token_duration,omitempty"`
MaxTokenDuration time.Duration `json:"max_token_duration,omitempty"`
DefaultTokenDuration Duration `json:"default_token_duration,omitempty"`
MaxTokenDuration Duration `json:"max_token_duration,omitempty"`
// Feature flags
Features map[string]bool `json:"features,omitempty"`
@ -83,7 +83,7 @@ type PasswordPolicy struct {
RequireLowercase bool `json:"require_lowercase"`
RequireNumbers bool `json:"require_numbers"`
RequireSymbols bool `json:"require_symbols"`
MaxAge time.Duration `json:"max_age,omitempty"`
MaxAge Duration `json:"max_age,omitempty"`
PreventReuse int `json:"prevent_reuse"` // Number of previous passwords to prevent reuse
}
@ -225,8 +225,8 @@ func (t *Tenant) GetAuthProvider() string {
// GetSessionTimeout returns the session timeout for the tenant
func (t *Tenant) GetSessionTimeout() time.Duration {
if t.Settings.SessionTimeout > 0 {
return t.Settings.SessionTimeout
if t.Settings.SessionTimeout.Duration > 0 {
return t.Settings.SessionTimeout.Duration
}
return 8 * time.Hour // default
}

View File

@ -48,8 +48,8 @@ func (r *ApplicationRepository) Create(ctx context.Context, app *domain.Applicat
pq.Array(typeStrings),
app.CallbackURL,
app.HMACKey,
app.TokenRenewalDuration.Nanoseconds(),
app.MaxTokenDuration.Nanoseconds(),
app.TokenRenewalDuration.Duration.Nanoseconds(),
app.MaxTokenDuration.Duration.Nanoseconds(),
string(app.Owner.Type),
app.Owner.Name,
app.Owner.Owner,
@ -118,8 +118,8 @@ func (r *ApplicationRepository) GetByID(ctx context.Context, appID string) (*dom
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
app.TokenRenewalDuration = domain.Duration{Duration: time.Duration(tokenRenewalNanos)}
app.MaxTokenDuration = domain.Duration{Duration: time.Duration(maxTokenNanos)}
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
@ -180,8 +180,8 @@ func (r *ApplicationRepository) List(ctx context.Context, limit, offset int) ([]
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
app.TokenRenewalDuration = domain.Duration{Duration: time.Duration(tokenRenewalNanos)}
app.MaxTokenDuration = domain.Duration{Duration: time.Duration(maxTokenNanos)}
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
@ -233,13 +233,13 @@ func (r *ApplicationRepository) Update(ctx context.Context, appID string, update
if updates.TokenRenewalDuration != nil {
setParts = append(setParts, fmt.Sprintf("token_renewal_duration = $%d", argIndex))
args = append(args, updates.TokenRenewalDuration.Nanoseconds())
args = append(args, updates.TokenRenewalDuration.Duration.Nanoseconds())
argIndex++
}
if updates.MaxTokenDuration != nil {
setParts = append(setParts, fmt.Sprintf("max_token_duration = $%d", argIndex))
args = append(args, updates.MaxTokenDuration.Nanoseconds())
args = append(args, updates.MaxTokenDuration.Duration.Nanoseconds())
argIndex++
}

View File

@ -375,7 +375,7 @@ func (s *sessionService) CreateOAuth2Session(ctx context.Context, userID, appID
expiresAt := time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second)
// Use application's max token duration if shorter
maxExpiration := time.Now().Add(app.MaxTokenDuration)
maxExpiration := time.Now().Add(app.MaxTokenDuration.Duration)
if expiresAt.After(maxExpiration) {
expiresAt = maxExpiration
}