Merge pull request #42 from Freika/api-key

Api Key authentication
This commit is contained in:
Evgenii Burmakin 2024-05-25 20:51:48 +02:00 committed by GitHub
commit fdf7d6f4a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 350 additions and 99 deletions

View file

@ -1 +1 @@
0.3.2
0.4.0

View file

@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.4.0] — 2024-05-25
**BREAKING CHANGES**:
- `/api/v1/points` is still working, but will be **deprecated** in nearest future. Please use `/api/v1/owntracks/points` instead.
- All existing points recorded directly to the database via Owntracks or Overland will be attached to the user with id 1.
### Added
- Each user now have an api key, which is required to make requests to the API. You can find your api key in your profile settings.
- You can re-generate your api key in your profile settings.
- In your user profile settings you can now see the instructions on how to use the API with your api key for both OwnTracks and Overland.
- Added docs on how to use the API with your api key. Refer to `/api-docs` for more information.
- `POST /api/v1/owntracks/points` endpoint.
- Points are now being attached to a user directly, so you can only see your own points and no other users of your applications can see your points.
### Changed
- `/api/v1/overland/batches` endpoint now requires an api key to be passed in the url. You can find your api key in your profile settings.
- All existing points recorded directly to the database will be attached to the user with id 1.
- All stats and maps are now being calculated and rendered based on the user's points only.
- Default `TIME_ZONE` environment variable is now set to 'UTC' in the `docker-compose.yml` file.
### Fixed
- Fixed a bug where marker on the map was rendering timestamp without considering the timezone.
---
## [0.3.2] — 2024-05-23
### Added

View file

@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.2.3'
gem 'bootsnap', require: false
gem 'chartkick'
gem 'data_migrate'
gem 'devise'
gem 'geocoder'
gem 'importmap-rails'

View file

@ -96,6 +96,9 @@ GEM
rexml
crass (1.0.6)
csv (3.3.0)
data_migrate (9.4.0)
activerecord (>= 6.1)
railties (>= 6.1)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
@ -391,6 +394,7 @@ PLATFORMS
DEPENDENCIES
bootsnap
chartkick
data_migrate
debug
devise
dotenv-rails

File diff suppressed because one or more lines are too long

View file

@ -2,9 +2,10 @@
class Api::V1::Overland::BatchesController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
def create
Overland::BatchCreatingJob.perform_later(batch_params)
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)
render json: { result: 'ok' }, status: :created
end
@ -12,6 +13,6 @@ class Api::V1::Overland::BatchesController < ApplicationController
private
def batch_params
params.permit(locations: [:type, geometry: {}, properties: {}], batch: {})
params.permit(locations: [:type, { geometry: {}, properties: {} }], batch: {})
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Api::V1::Owntracks::PointsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
def create
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)
render json: {}, status: :ok
end
private
def point_params
params.permit!
end
end

View file

@ -1,21 +1,17 @@
# frozen_string_literal: true
# TODO: Deprecate in 1.0
class Api::V1::PointsController < ApplicationController
skip_forgery_protection
def create
Rails.logger.info 'This endpoint will be deprecated in 1.0. Use /api/v1/owntracks/points instead'
Owntracks::PointCreatingJob.perform_later(point_params)
render json: {}, status: :ok
end
def destroy
@point = Point.find(params[:id])
@point.destroy
head :no_content
end
private
def point_params

View file

@ -1,3 +1,17 @@
# frozen_string_literal: true
class ApplicationController < ActionController::Base
include Pundit::Authorization
protected
def authenticate_api_key
return head :unauthorized unless current_api_user
true
end
def current_api_user
@current_api_user ||= User.find_by(api_key: params[:api_key])
end
end

View file

@ -24,7 +24,7 @@ class ExportController < ApplicationController
end
def start_at
first_point_timestamp = Point.order(timestamp: :asc)&.first&.timestamp
first_point_timestamp = current_user.tracked_points.order(timestamp: :asc)&.first&.timestamp
@start_at ||=
if params[:start_at].nil? && first_point_timestamp.present?
@ -37,7 +37,7 @@ class ExportController < ApplicationController
end
def end_at
last_point_timestamp = Point.order(timestamp: :desc)&.last&.timestamp
last_point_timestamp = current_user.tracked_points.order(timestamp: :desc)&.last&.timestamp
@end_at ||=
if params[:end_at].nil? && last_point_timestamp.present?

View file

@ -4,6 +4,6 @@ class HomeController < ApplicationController
def index
redirect_to map_url if current_user
@points = current_user.points.without_raw_data if current_user
@points = current_user.tracked_points.without_raw_data if current_user
end
end

View file

@ -3,7 +3,7 @@ class MapController < ApplicationController
before_action :authenticate_user!
def index
@points = Point.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc)
@points = current_user.tracked_points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc)
@countries_and_cities = CountriesAndCities.new(@points).call
@coordinates =

View file

@ -5,7 +5,8 @@ class PointsController < ApplicationController
def index
@points =
Point
current_user
.tracked_points
.without_raw_data
.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
.order(timestamp: :asc)
@ -16,9 +17,9 @@ class PointsController < ApplicationController
end
def bulk_destroy
Point.where(id: params[:point_ids].compact).destroy_all
current_user.tracked_points.where(id: params[:point_ids].compact).destroy_all
redirect_to points_url, notice: "Points were successfully destroyed.", status: :see_other
redirect_to points_url, notice: 'Points were successfully destroyed.', status: :see_other
end
private

View file

@ -8,4 +8,10 @@ class SettingsController < ApplicationController
redirect_back(fallback_location: root_path)
end
def generate_api_key
current_user.update(api_key: SecureRandom.hex)
redirect_back(fallback_location: root_path)
end
end

View file

@ -37,24 +37,24 @@ module ApplicationHelper
%w[info success warning error accent secondary primary]
end
def countries_and_cities_stat(year)
data = Stat.year_cities_and_countries(year)
def countries_and_cities_stat(year, user)
data = Stat.year_cities_and_countries(year, user)
countries = data[:countries]
cities = data[:cities]
"#{countries} countries, #{cities} cities"
end
def year_distance_stat_in_km(year)
Stat.year_distance(year).sum { _1[1] }
def year_distance_stat_in_km(year, user)
Stat.year_distance(year, user).sum { _1[1] }
end
def past?(year, month)
DateTime.new(year, month).past?
end
def points_exist?(year, month)
Point.where(
def points_exist?(year, month, user)
user.tracked_points.where(
timestamp: DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
).exists?
end

View file

@ -96,15 +96,9 @@ export default class extends Controller {
formatDate(timestamp) {
let date = new Date(timestamp * 1000); // Multiply by 1000 because JavaScript works with milliseconds
// Extracting date components
let year = date.getFullYear();
let month = ('0' + (date.getMonth() + 1)).slice(-2); // Adding 1 because getMonth() returns zero-based month
let day = ('0' + date.getDate()).slice(-2);
let hours = ('0' + date.getHours()).slice(-2);
let minutes = ('0' + date.getMinutes()).slice(-2);
let seconds = ('0' + date.getSeconds()).slice(-2);
let timezone = this.element.dataset.timezone;
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
return date.toLocaleString('en-GB', { timeZone: timezone });
}
addTileLayer(map) {

View file

@ -7,7 +7,7 @@ class ImportJob < ApplicationJob
user = User.find(user_id)
import = user.imports.find(import_id)
result = parser(import.source).new(import).call
result = parser(import.source).new(import, user_id).call
import.update(
raw_points: result[:raw_points], doubles: result[:doubles], processed: result[:processed]

View file

@ -3,11 +3,11 @@
class Overland::BatchCreatingJob < ApplicationJob
queue_as :default
def perform(params)
def perform(params, user_id)
data = Overland::Params.new(params).call
data.each do |location|
Point.create!(location)
Point.create!(location.merge(user_id:))
end
end
end

View file

@ -3,9 +3,10 @@
class Owntracks::PointCreatingJob < ApplicationJob
queue_as :default
def perform(point_params)
# TODO: after deprecation of old endpoint, make user_id required
def perform(point_params, user_id = nil)
parsed_params = OwnTracks::Params.new(point_params).call
Point.create(parsed_params)
Point.create!(parsed_params.merge(user_id:))
end
end

View file

@ -4,6 +4,7 @@ class Point < ApplicationRecord
# self.ignored_columns = %w[raw_data]
belongs_to :import, optional: true
belongs_to :user, optional: true
validates :latitude, :longitude, :timestamp, presence: true

View file

@ -11,7 +11,7 @@ class Stat < ApplicationRecord
end_of_day = day.end_of_day.to_i
# We have to filter by user as well
points = Point.without_raw_data.where(timestamp: beginning_of_day..end_of_day)
points = user.tracked_points.without_raw_data.where(timestamp: beginning_of_day..end_of_day)
data = { day: index, distance: 0 }
@ -27,8 +27,8 @@ class Stat < ApplicationRecord
end
end
def self.year_distance(year)
stats = where(year: year).order(:month)
def self.year_distance(year, user)
stats = where(year:, user:).order(:month)
(1..12).to_a.map do |month|
month_stat = stats.select { |stat| stat.month == month }.first
@ -40,8 +40,8 @@ class Stat < ApplicationRecord
end
end
def self.year_cities_and_countries(year)
points = Point.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year)
def self.year_cities_and_countries(year, user)
points = user.tracked_points.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year)
data = CountriesAndCities.new(points).call

View file

@ -9,6 +9,7 @@ class User < ApplicationRecord
has_many :imports, dependent: :destroy
has_many :points, through: :imports
has_many :stats, dependent: :destroy
has_many :tracked_points, class_name: 'Point', dependent: :destroy
after_create :create_api_key

View file

@ -16,7 +16,7 @@ class CreateStats
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
points = points(user, beginning_of_month_timestamp, end_of_month_timestamp)
next if points.empty?
stat = Stat.find_or_initialize_by(year:, month:, user:)
@ -31,8 +31,9 @@ class CreateStats
private
def points(beginning_of_month_timestamp, end_of_month_timestamp)
Point
def points(user, beginning_of_month_timestamp, end_of_month_timestamp)
user
.tracked_points
.without_raw_data
.where(timestamp: beginning_of_month_timestamp..end_of_month_timestamp)
.order(:timestamp)

View file

@ -1,10 +1,11 @@
# frozen_string_literal: true
class GoogleMaps::SemanticHistoryParser
attr_reader :import
attr_reader :import, :user_id
def initialize(import)
def initialize(import, user_id)
@import = import
@user_id = user_id
end
def call
@ -22,7 +23,8 @@ class GoogleMaps::SemanticHistoryParser
raw_data: point_data[:raw_data],
topic: 'Google Maps Timeline Export',
tracker_id: 'google-maps-timeline-export',
import_id: import.id
import_id: import.id,
user_id:
)
points += 1

View file

@ -1,11 +1,12 @@
# frozen_string_literal: true
class OwnTracks::ExportParser
attr_reader :import, :json
attr_reader :import, :json, :user_id
def initialize(import)
def initialize(import, user_id)
@import = import
@json = import.raw_data
@user_id = user_id
end
def call
@ -23,7 +24,8 @@ class OwnTracks::ExportParser
raw_data: point_data[:raw_data],
topic: point_data[:topic],
tracker_id: point_data[:tracker_id],
import_id: import.id
import_id: import.id,
user_id:
)
points += 1

View file

@ -2,8 +2,21 @@
<p class='py-2'>Use this API key to authenticate your requests.</p>
<code><%= current_user.api_key %></code>
<p class='py-2'>
<p>Docs: <%= link_to "API documentation", '/api-docs', class: 'underline hover:no-underline' %></p>
Usage example:
<p><code><%= api_v1_points_url(api_key: current_user.api_key) %></code></p>
<div role="tablist" class="tabs tabs-boxed">
<input type="radio" name="my_tabs_2" role="tab" class="tab" aria-label="OwnTracks" />
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6">
<p><code><%= api_v1_owntracks_points_url(api_key: current_user.api_key) %></code></p>
</div>
<input type="radio" name="my_tabs_2" role="tab" class="tab" aria-label="Overland" checked />
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6">
<p><code><%= api_v1_overland_batches_url(api_key: current_user.api_key) %></code></p>
</div>
</div>
</p>
<p class='py-2'>
<%= link_to "Generate new API key", generate_api_key_path, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :post }, class: 'btn btn-primary' %>

View file

@ -2,7 +2,7 @@
<div class="hero-content flex-col lg:flex-row-reverse w-full my-10">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Edit your account!</h1>
<%#= render 'devise/registrations/api_key' %>
<%= render 'devise/registrations/api_key' %>
</div>
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 px-5 py-5">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), class: 'form-body', html: { method: :put }) do |f| %>

View file

@ -22,11 +22,17 @@
</div>
<% end %>
<div role="alert" class="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span>Warning: Starting release 0.4.0 it's HIGHLY RECOMMENDED to switch from <code>/api/v1/points</code> to <code>/api/v1/owntracks/points</code> API endpoint. Please read more at <a href="https://github.com/Freika/dawarich/releases/tag/0.4.0" class='underline hover:no-underline'>0.4.0 release notes</a></span>
</div>
<div
class="w-full"
data-controller="maps"
data-coordinates="<%= @coordinates %>"
data-center="<%= MAP_CENTER %>">
data-center="<%= MAP_CENTER %>"
data-timezone="<%= Rails.configuration.time_zone %>">
<div data-maps-target="container" class="h-[25rem] w-auto min-h-screen"></div>
</div>
</div>

View file

@ -4,7 +4,7 @@
<div class="dropdown">
<div tabindex="0" role="button" class="btn m-1">Select year</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<% Stat.years.each do |year| %>
<% current_user.stats.years.each do |year| %>
<li><%= link_to year, map_url(year_timespan(year).merge(year: year)) %></li>
<% end %>
</ul>
@ -18,7 +18,7 @@
<div class='grid grid-cols-3 gap-3'>
<% (1..12).to_a.each_slice(3) do |months| %>
<% months.each do |month_number| %>
<% if past?(year, month_number) && points_exist?(year, month_number) %>
<% if past?(year, month_number) && points_exist?(year, month_number, current_user) %>
<%= link_to Date::ABBR_MONTHNAMES[month_number], map_url(timespan(month_number, year)), class: 'btn btn-default' %>
<% else %>
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>

View file

@ -4,7 +4,7 @@
</h2>
<div class='my-10'>
<%= column_chart(
Stat.year_distance(year),
Stat.year_distance(year, current_user),
height: '200px',
suffix: ' km',
xtitle: 'Days',

View file

@ -9,7 +9,7 @@
<div class="stat text-center">
<div class="stat-value text-success">
<%= number_with_delimiter current_user.points.without_raw_data.count(:id) %>
<%= number_with_delimiter current_user.tracked_points.without_raw_data.count(:id) %>
</div>
<div class="stat-title">Geopoints tracked</div>
</div>
@ -77,18 +77,18 @@
</h2>
<p>
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
<%= number_with_delimiter year_distance_stat_in_km(year) %>km
<%= number_with_delimiter year_distance_stat_in_km(year, current_user) %>km
<% end %>
</p>
<% if REVERSE_GEOCODING_ENABLED %>
<div class="card-actions justify-end">
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %>
<%= countries_and_cities_stat(year) %>
<%= countries_and_cities_stat(year, current_user) %>
<% end %>
</div>
<% end %>
<%= column_chart(
Stat.year_distance(year),
Stat.year_distance(year, current_user),
height: '200px',
suffix: ' km',
xtitle: 'Days',

View file

@ -6,6 +6,7 @@ Rails.application.routes.draw do
mount Rswag::Api::Engine => '/api-docs'
mount Rswag::Ui::Engine => '/api-docs'
mount Sidekiq::Web => '/sidekiq'
get 'settings/theme', to: 'settings#theme'
get 'export', to: 'export#index'
get 'export/download', to: 'export#download'
@ -26,17 +27,21 @@ Rails.application.routes.draw do
root to: 'home#index'
devise_for :users
post 'settings/generate_api_key', to: 'devise/api_keys#create', as: :generate_api_key
post 'settings/generate_api_key', to: 'settings#generate_api_key', as: :generate_api_key
get 'map', to: 'map#index'
namespace :api do
namespace :v1 do
resources :points
resources :points, only: :create # TODO: Deprecate in 1.0
namespace :overland do
resources :batches, only: :create
end
namespace :owntracks do
resources :points, only: :create
end
end
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class BindExistingPointsToFirstUser < ActiveRecord::Migration[7.1]
def up
user = User.first
return if user.blank?
points = Point.where(user_id: nil)
points.update_all(user_id: user.id)
Rails.logger.info "Bound #{points.count} points to user #{user.email}"
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

1
db/data_schema.rb Normal file
View file

@ -0,0 +1 @@
DataMigrate::Data.define(version: 20240525110530)

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddUserIdToPoints < ActiveRecord::Migration[7.1]
def change
add_reference :points, :user, foreign_key: true
end
end

5
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
ActiveRecord::Schema[7.1].define(version: 2024_05_25_110244) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -82,6 +82,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
t.string "country"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id"
t.index ["altitude"], name: "index_points_on_altitude"
t.index ["battery"], name: "index_points_on_battery"
t.index ["battery_status"], name: "index_points_on_battery_status"
@ -92,6 +93,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude"
t.index ["timestamp"], name: "index_points_on_timestamp"
t.index ["trigger"], name: "index_points_on_trigger"
t.index ["user_id"], name: "index_points_on_user_id"
end
create_table "stats", force: :cascade do |t|
@ -125,5 +127,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "points", "users"
add_foreign_key "stats", "users"
end

View file

@ -28,5 +28,9 @@ bundle exec rails db:create
echo "PostgreSQL is ready. Running database migrations..."
bundle exec rails db:prepare
# Run data migrations
echo "Running DATA migrations..."
bundle exec rake data:migrate
# run passed commands
bundle exec ${@}

View file

@ -44,6 +44,7 @@ services:
DATABASE_NAME: dawarich_development
MIN_MINUTES_SPENT_IN_CITY: 60
APPLICATION_HOST: localhost
TIME_ZONE: UTC
depends_on:
- dawarich_db
- dawarich_redis

View file

@ -2,14 +2,21 @@ require 'rails_helper'
RSpec.describe Overland::BatchCreatingJob, type: :job do
describe '#perform' do
subject(:perform) { described_class.new.perform(json) }
subject(:perform) { described_class.new.perform(json, user.id) }
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:user) { create(:user) }
it 'creates a location' do
expect { perform }.to change { Point.count }.by(1)
end
it 'creates a point with the correct user_id' do
perform
expect(Point.last.user_id).to eq(user.id)
end
end
end

View file

@ -2,14 +2,21 @@ require 'rails_helper'
RSpec.describe Owntracks::PointCreatingJob, type: :job do
describe '#perform' do
subject(:perform) { described_class.new.perform(point_params) }
subject(:perform) { described_class.new.perform(point_params, user.id) }
let(:point_params) do
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
end
let(:user) { create(:user) }
it 'creates a point' do
expect { perform }.to change { Point.count }.by(1)
end
it 'creates a point with the correct user_id' do
perform
expect(Point.last.user_id).to eq(user.id)
end
end
end

View file

@ -1,8 +1,11 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Point, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:import).optional }
it { is_expected.to belong_to(:user).optional }
end
describe 'validations' do

View file

@ -11,9 +11,10 @@ RSpec.describe Stat, type: :model do
describe 'methods' do
let(:year) { 2021 }
let(:user) { create(:user) }
describe '.year_cities_and_countries' do
subject { described_class.year_cities_and_countries(year) }
subject { described_class.year_cities_and_countries(year, user) }
let(:timestamp) { DateTime.new(year, 1, 1, 0, 0, 0) }
@ -24,16 +25,16 @@ RSpec.describe Stat, type: :model do
context 'when there are points' do
let!(:points) do
[
create(:point, city: 'Berlin', country: 'Germany', timestamp:),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp:),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
]
end
@ -86,8 +87,8 @@ RSpec.describe Stat, type: :model do
context 'when there are points' do
let!(:points) do
create(:point, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1))
create(:point, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2))
create(:point, user:, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1))
create(:point, user:, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2))
end
before { expected_distance[0][1] = 157.23 }
@ -116,7 +117,7 @@ RSpec.describe Stat, type: :model do
end
describe '#self.year_distance' do
subject { described_class.year_distance(year) }
subject { described_class.year_distance(year, user) }
let(:user) { create(:user) }
let(:expected_distance) do

View file

@ -7,6 +7,7 @@ RSpec.describe User, type: :model do
it { is_expected.to have_many(:imports).dependent(:destroy) }
it { is_expected.to have_many(:points).through(:imports) }
it { is_expected.to have_many(:stats) }
it { is_expected.to have_many(:tracked_points).class_name('Point').dependent(:destroy) }
end
describe 'callbacks' do

View file

@ -9,16 +9,28 @@ RSpec.describe 'Api::V1::Overland::Batches', type: :request do
let(:json) { JSON.parse(file.read) }
let(:params) { json }
it 'returns http success' do
post '/api/v1/overland/batches', params: params
context 'with invalid api key' do
it 'returns http unauthorized' do
post '/api/v1/overland/batches', params: params
expect(response).to have_http_status(:created)
expect(response).to have_http_status(:unauthorized)
end
end
it 'enqueues a job' do
expect do
post '/api/v1/overland/batches', params: params
end.to have_enqueued_job(Overland::BatchCreatingJob)
context 'with valid api key' do
let(:user) { create(:user) }
it 'returns http success' do
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
expect(response).to have_http_status(:created)
end
it 'enqueues a job' do
expect do
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
end.to have_enqueued_job(Overland::BatchCreatingJob)
end
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Owntracks::Points', type: :request do
describe 'POST /api/v1/owntracks/points' do
context 'with valid params' do
let(:params) do
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.current.to_i, topic: 'iPhone 12 pro' }
end
let(:user) { create(:user) }
context 'with invalid api key' do
it 'returns http unauthorized' do
post api_v1_owntracks_points_path, params: params
expect(response).to have_http_status(:unauthorized)
end
end
context 'with valid api key' do
it 'returns http success' do
post api_v1_owntracks_points_path(api_key: user.api_key), params: params
expect(response).to have_http_status(:success)
end
it 'enqueues a job' do
expect do
post api_v1_owntracks_points_path(api_key: user.api_key), params: params
end.to have_enqueued_job(Owntracks::PointCreatingJob)
end
end
end
end
end

View file

@ -30,11 +30,12 @@ RSpec.describe '/points', type: :request do
end
describe 'DELETE /bulk_destroy' do
let(:point1) { create(:point) }
let(:point2) { create(:point) }
let(:user) { create(:user) }
let(:point1) { create(:point, user:) }
let(:point2) { create(:point, user:) }
before do
sign_in create(:user)
sign_in user
end
it 'destroys the selected points' do

View file

@ -40,4 +40,32 @@ RSpec.describe 'Settings', type: :request do
end
end
end
describe 'POST /generate_api_key' do
context 'when user is not signed in' do
it 'redirects to the sign in page' do
post '/settings/generate_api_key'
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when user is signed in' do
let(:user) { create(:user) }
before do
sign_in user
end
it 'generates an API key for the user' do
expect { post '/settings/generate_api_key' }.to change { user.reload.api_key }
end
it 'redirects back' do
post '/settings/generate_api_key'
expect(response).to redirect_to(root_path)
end
end
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe CreateStats do
@ -9,16 +11,15 @@ RSpec.describe CreateStats do
context 'when there are no points' do
it 'does not create stats' do
expect { create_stats }.not_to change { Stat.count }
expect { create_stats }.not_to(change { Stat.count })
end
end
context 'when there are points' do
let!(:import) { create(:import, user: user) }
let!(:point_1) { create(:point, import: import, latitude: 0, longitude: 0) }
let!(:point_2) { create(:point, import: import, latitude: 1, longitude: 2) }
let!(:point_3) { create(:point, import: import, latitude: 3, longitude: 4) }
let!(:import) { create(:import, user:) }
let!(:point1) { create(:point, user:, import:, latitude: 0, longitude: 0) }
let!(:point2) { create(:point, user:, import:, latitude: 1, longitude: 2) }
let!(:point3) { create(:point, user:, import:, latitude: 3, longitude: 4) }
it 'creates stats' do
expect { create_stats }.to change { Stat.count }.by(1)

View file

@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe OwnTracks::ExportParser do
describe '#call' do
subject(:parser) { described_class.new(import).call }
subject(:parser) { described_class.new(import, user.id).call }
let(:user) { create(:user) }
let(:import) { create(:import, user:, name: 'owntracks_export.json') }

View file

@ -72,12 +72,26 @@ describe 'Batches API', type: :request do
}
}
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '201', 'Batch of points created' do
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:params) { json }
let(:locations) { params['locations'] }
let(:api_key) { create(:user).api_key }
run_test!
end
response '401', 'Unauthorized' do
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
let(:file) { File.open(file_path) }
let(:json) { JSON.parse(file.read) }
let(:params) { json }
let(:locations) { params['locations'] }
let(:api_key) { nil }
run_test!
end

View file

@ -9,10 +9,18 @@ paths:
summary: Creates a batch of points
tags:
- Batches
parameters: []
parameters:
- name: api_key
in: query
required: true
description: API Key
schema:
type: string
responses:
'201':
description: Batch of points created
'401':
description: Unauthorized
requestBody:
content:
application/json:
@ -172,7 +180,7 @@ paths:
lat: 52.502397
lon: 13.356718
tid: Swagger
tst: 1716488929
tst: 1716638918
servers:
- url: http://{defaultHost}
variables: