Create ApiToken model for REST API authentication.
- Task 003 completed
bin/rails g model ApiToken user:references tenant:references name:string token_digest:string prefix:string scopes:text last_used_at:datetime expires_at:datetime revoked_at:datetime
bin/rails db:migrateclass ApiToken < ApplicationRecord
belongs_to :user
belongs_to :tenant
validates :name, presence: true
validates :token_digest, presence: true, uniqueness: true
validates :prefix, presence: true
serialize :scopes, coder: JSON
scope :active, -> { where(revoked_at: nil).where('expires_at IS NULL OR expires_at > ?', Time.current) }
def self.generate(user:, tenant:, name:, scopes: [], expires_in: nil)
token = SecureRandom.hex(32)
api_token = create!(
user: user,
tenant: tenant,
name: name,
token_digest: Digest::SHA256.hexdigest(token),
prefix: token[0..7],
scopes: scopes,
expires_at: expires_in ? Time.current + expires_in : nil
)
[api_token, token] # Return both record and plain token
end
def self.find_by_token(token)
return nil unless token.present?
find_by(token_digest: Digest::SHA256.hexdigest(token))
end
def valid_for_use?
!revoked? && !expired?
end
def revoked?
revoked_at.present?
end
def expired?
expires_at.present? && expires_at <= Time.current
end
def revoke!
update!(revoked_at: Time.current)
end
endFactoryBot.define do
factory :api_token do
user
tenant
name { 'Test Token' }
token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(32)) }
prefix { SecureRandom.hex(4) }
scopes { ['read', 'write'] }
trait :expired do
expires_at { 1.day.ago }
end
trait :revoked do
revoked_at { 1.hour.ago }
end
end
endrequire 'rails_helper'
RSpec.describe 'Task 006: API Token', type: :task do
describe ApiToken do
let(:user) { create(:user) }
let(:tenant) { create(:tenant) }
it { expect(build(:api_token)).to be_valid }
it { should belong_to(:user) }
it { should belong_to(:tenant) }
it { should validate_presence_of(:name) }
describe '.generate' do
it 'creates token and returns plain token' do
api_token, plain_token = ApiToken.generate(
user: user, tenant: tenant, name: 'My Token'
)
expect(api_token).to be_persisted
expect(plain_token.length).to eq(64)
end
end
describe '.find_by_token' do
it 'finds token by plain token' do
api_token, plain_token = ApiToken.generate(
user: user, tenant: tenant, name: 'Test'
)
found = ApiToken.find_by_token(plain_token)
expect(found).to eq(api_token)
end
it 'returns nil for invalid token' do
expect(ApiToken.find_by_token('invalid')).to be_nil
end
end
describe '#valid_for_use?' do
it 'returns true for active token' do
api_token = create(:api_token)
expect(api_token.valid_for_use?).to be true
end
it 'returns false for expired token' do
api_token = create(:api_token, :expired)
expect(api_token.valid_for_use?).to be false
end
it 'returns false for revoked token' do
api_token = create(:api_token, :revoked)
expect(api_token.valid_for_use?).to be false
end
end
describe '#revoke!' do
it 'sets revoked_at' do
api_token = create(:api_token)
api_token.revoke!
expect(api_token.revoked?).to be true
end
end
end
endbundle exec rspec spec/tasks/task_006_spec.rb✅ All tests pass