summaryrefslogtreecommitdiff
path: root/app/models/token.rb
blob: 6d9f0fec7c2b2c7c86f4f05c6912f68e4a34da6d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
require 'digest/sha2'

class Token < CouchRest::Model::Base
  def self.expires_after
    APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after]
  end

  include CouchRest::Model::Rotation
  rotate_database :tokens,
    :every => 1.month,
    :timestamp_field => :last_seen_at,
    :timeout => self.expires_after.to_i # in minutes

  belongs_to :user

  # timestamps! does not create setters and only sets updated_at
  # if the object has changed and been saved. Instead of triggering
  # that we rather use our own property we have control over:
  property :last_seen_at, Time, accessible: false

  validates :user_id, presence: true

  attr_accessor :token

  design do
    view :by_last_seen_at
  end

  def self.find_by_token(token)
    self.find Digest::SHA512.hexdigest(token)
  end

  def self.expired
    return [] unless expires_after
    by_last_seen_at.endkey(expires_after.minutes.ago)
  end

  def self.to_cleanup
    return [] unless expires_after
    by_last_seen_at.endkey((expires_after + 5).minutes.ago)
  end

  def self.destroy_all_expired
    self.to_cleanup.each do |token|
      token.destroy
    end
  end

  def to_s
    token
  end

  def authenticate
    return if expired?
    touch
    return user
  rescue CouchRest::NotFound
    # Reload in touch failed - token has been deleted.
    # That's either an active logout or account destruction.
    # We don't accept the token anymore.
  end

  # Tokens can be cleaned up in different ways.
  # So let's make sure we don't crash if they disappeared
  def destroy_with_rescue
    destroy_without_rescue
  rescue CouchRest::Conflict # do nothing - it's been updated - #7670
    try_to_reload && retry
  rescue CouchRest::NotFound
  end
  alias_method_chain :destroy, :rescue

  def touch
    update_attributes last_seen_at: Time.now
  rescue CouchRest::Conflict
    reload && retry
  end

  def expired?
    Token.expires_after and
    last_seen_at < Token.expires_after.minutes.ago
  end

  def initialize(*args)
    super
    if new_record?
      self.token = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '')
      self.id = Digest::SHA512.hexdigest(self.token)
      self.last_seen_at = Time.now
    end
  end

  # UPGRADE: the underlying code here changes between CouchRest::Model
  # 2.1.0.rc1 and 2.2.0.beta2
  # Hopefully we'll also get a pr merged that pushes this workaround
  # upstream:
  # https://github.com/couchrest/couchrest_model/pull/223
  def reload
    prepare_all_attributes(
      database.get!(id), :directly_set_attributes => true
    )
    self
  end

  protected

  def try_to_reload
    reload
  rescue CouchRest::NotFound
    return false
  end

end