Class: Tina4::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/session.rb

Constant Summary collapse

DEFAULT_OPTIONS =
{
  cookie_name: "tina4_session",
  secret: nil,
  max_age: 3600,
  handler: :file,
  handler_options: {}
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env, options = {}) ⇒ Session

Returns a new instance of Session.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/tina4/session.rb', line 17

def initialize(env, options = {})
  @options = DEFAULT_OPTIONS.merge(options)
  # TINA4_SESSION_NAME — overrides cookie_name unless caller explicitly passed one.
  env_name = ENV["TINA4_SESSION_NAME"]
  if !options.key?(:cookie_name) && env_name && !env_name.empty?
    @options[:cookie_name] = env_name
  end
  # No guessable built-in secret. The session never signs with this value
  # (IDs are SecureRandom.hex(32)), so we resolve it from TINA4_SECRET only
  # — nil when unset. This honours the framework's blank-secret discipline
  # (Auth.ensure_dev_secret never uses a guessable default); Python/Node
  # sessions carry no secret field at all.
  @options[:secret] ||= ENV["TINA4_SECRET"]
  # Backend-failure policy strict flag (parity with Python's
  # TINA4_SESSION_STRICT). When truthy, read/write/destroy/gc failures
  # RE-RAISE instead of logging + degrading.
  @strict = Tina4::Env.is_truthy(ENV["TINA4_SESSION_STRICT"])
  @handler = create_handler
  @id = extract_session_id(env) || SecureRandom.hex(32)
  @data = load_session
  @modified = false
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



15
16
17
# File 'lib/tina4/session.rb', line 15

def data
  @data
end

#idObject (readonly)

Returns the value of attribute id.



15
16
17
# File 'lib/tina4/session.rb', line 15

def id
  @id
end

Instance Method Details

#[](key) ⇒ Object



40
41
42
# File 'lib/tina4/session.rb', line 40

def [](key)
  @data[key.to_s]
end

#[]=(key, value) ⇒ Object



44
45
46
47
# File 'lib/tina4/session.rb', line 44

def []=(key, value)
  @data[key.to_s] = value
  @modified = true
end

#allObject

Return all session data



100
101
102
# File 'lib/tina4/session.rb', line 100

def all
  @data.dup
end

#clearObject



54
55
56
57
# File 'lib/tina4/session.rb', line 54

def clear
  @data = {}
  @modified = true
end


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/tina4/session.rb', line 182

def cookie_header(cookie_name = nil)
  name = cookie_name || @options[:cookie_name]
  samesite = ENV["TINA4_SESSION_SAMESITE"] || "Lax"
  # HttpOnly defaults to true (existing behaviour); flip off only when explicitly false.
  httponly = !%w[false 0 no off].include?((ENV["TINA4_SESSION_HTTPONLY"] || "true").to_s.strip.downcase)
  # Secure defaults to false; flip on with truthy env var.
  secure = %w[true 1 yes on].include?((ENV["TINA4_SESSION_SECURE"] || "false").to_s.strip.downcase)

  parts = ["#{name}=#{@id}", "Path=/"]
  parts << "HttpOnly" if httponly
  parts << "Secure" if secure
  parts << "SameSite=#{samesite}"
  parts << "Max-Age=#{@options[:max_age]}"
  parts.join("; ")
end

#delete(key) ⇒ Object



49
50
51
52
# File 'lib/tina4/session.rb', line 49

def delete(key)
  @data.delete(key.to_s)
  @modified = true
end

#destroyObject

Destroy the current session. Should be called right after login or any privilege change to defend against session fixation (see #regenerate).



78
79
80
81
# File 'lib/tina4/session.rb', line 78

def destroy
  safe_destroy(@id)
  @data = {}
end

#flash(key, value = nil) ⇒ Object

Flash data: set a value that is removed after next read. Call with value to set, call without value to get (and remove).



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/tina4/session.rb', line 106

def flash(key, value = nil)
  flash_key = "_flash_#{key}"
  if value.nil?
    val = @data.delete(flash_key.to_s)
    @modified = true if val
    val
  else
    @data[flash_key.to_s] = value
    @modified = true
    value
  end
end

#gc(max_lifetime = nil) ⇒ Object

Garbage collection: remove expired sessions from the handler. A backend failure is logged and swallowed (never crashes the request).



172
173
174
175
176
177
178
179
180
# File 'lib/tina4/session.rb', line 172

def gc(max_lifetime = nil)
  return unless @handler.respond_to?(:gc)
  max_lifetime ||= @options[:max_age]
  @handler.gc(max_lifetime)
rescue StandardError => e
  log_backend_error("gc", e)
  raise if @strict
  nil
end

#get(key, default = nil) ⇒ Object

Get a session value with optional default



84
85
86
# File 'lib/tina4/session.rb', line 84

def get(key, default = nil)
  @data[key.to_s] || default
end

#get_flash(key, default = nil) ⇒ Object

Get flash data by key (alias for flash(key) without value)



120
121
122
123
# File 'lib/tina4/session.rb', line 120

def get_flash(key, default = nil)
  result = flash(key)
  result.nil? ? default : result
end

#get_session_idObject

Returns the current session ID string.



154
155
156
# File 'lib/tina4/session.rb', line 154

def get_session_id
  @id
end

#has?(key) ⇒ Boolean

Check if a key exists in the session

Returns:

  • (Boolean)


95
96
97
# File 'lib/tina4/session.rb', line 95

def has?(key)
  @data.key?(key.to_s)
end

#read(session_id) ⇒ Object

Reads raw session data for a given session ID from backend storage. Returns the data hash, or {} on a backend failure (logged + degraded).



160
161
162
# File 'lib/tina4/session.rb', line 160

def read(session_id)
  safe_read(session_id)
end

#regenerateObject

Regenerate the session ID while preserving data — returns the new ID. Call this right after login or any privilege change to defend against session fixation (a pre-auth session ID must not survive into the authenticated session). Destroys the old backend record (best-effort) and persists under the new ID.



130
131
132
133
134
135
136
137
# File 'lib/tina4/session.rb', line 130

def regenerate
  old_id = @id
  @id = SecureRandom.hex(32)
  safe_destroy(old_id)
  @modified = true
  save
  @id
end

#saveObject

Persist the session if dirty. On a backend write failure the error is logged and false is returned — the @modified (dirty) flag is RETAINED so a later save can retry. Returns true on a successful (or no-op) write.



66
67
68
69
70
71
72
73
74
# File 'lib/tina4/session.rb', line 66

def save
  return true unless @modified
  if safe_write(@id, @data)
    @modified = false
    true
  else
    false # dirty flag retained for retry
  end
end

#set(key, value) ⇒ Object

Set a session value



89
90
91
92
# File 'lib/tina4/session.rb', line 89

def set(key, value)
  @data[key.to_s] = value
  @modified = true
end

#start(session_id = nil) ⇒ Object

Start or resume a session. If session_id is given, load that session; otherwise generate a new ID. Returns the session ID string.



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/tina4/session.rb', line 141

def start(session_id = nil)
  if session_id
    @id = session_id
    @data = load_session
  else
    @id = SecureRandom.hex(32)
    @data = {}
  end
  @modified = false
  @id
end

#to_hashObject



59
60
61
# File 'lib/tina4/session.rb', line 59

def to_hash
  @data.dup
end

#write(session_id, data, ttl = nil) ⇒ Object

Writes raw session data for a given session ID to backend storage. Returns true on success, false on a backend failure (logged + degraded).



166
167
168
# File 'lib/tina4/session.rb', line 166

def write(session_id, data, ttl = nil)
  safe_write(session_id, data, ttl)
end