From 5ae07f5643df790aaa32e7e1210d6647a06b336d Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 3 Oct 2025 18:22:37 -0400 Subject: [PATCH] Patch up oauth implementation (#560) --- .../v1/authenticated/heartbeats_controller.rb | 30 ++++++++++++ .../api/v1/authenticated/hours_controller.rb | 22 +++++++++ .../v1/authenticated/projects_controller.rb | 47 +++++++++++++++++++ .../api/v1/authenticated/streak_controller.rb | 13 +++++ app/models/user.rb | 10 ++++ config/initializers/doorkeeper.rb | 11 +++++ config/routes.rb | 6 +++ ...161836_add_foreign_keys_to_oauth_access.rb | 6 +++ db/schema.rb | 4 +- db/seeds.rb | 8 ++++ 10 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/authenticated/heartbeats_controller.rb create mode 100644 app/controllers/api/v1/authenticated/hours_controller.rb create mode 100644 app/controllers/api/v1/authenticated/projects_controller.rb create mode 100644 app/controllers/api/v1/authenticated/streak_controller.rb create mode 100644 db/migrate/20251003161836_add_foreign_keys_to_oauth_access.rb diff --git a/app/controllers/api/v1/authenticated/heartbeats_controller.rb b/app/controllers/api/v1/authenticated/heartbeats_controller.rb new file mode 100644 index 0000000..221a99e --- /dev/null +++ b/app/controllers/api/v1/authenticated/heartbeats_controller.rb @@ -0,0 +1,30 @@ +module Api + module V1 + module Authenticated + class HeartbeatsController < ApplicationController + def latest + heartbeat = current_user.heartbeats + .where.not(source_type: :test_entry) + .order(time: :desc) + .first + + if heartbeat + render json: { + id: heartbeat.id, + created_at: heartbeat.created_at, + project: heartbeat.project, + language: heartbeat.language, + editor: heartbeat.editor, + operating_system: heartbeat.operating_system, + machine: heartbeat.machine, + file: heartbeat.file, + duration: heartbeat.duration + } + else + render json: { heartbeat: nil } + end + end + end + end + end +end diff --git a/app/controllers/api/v1/authenticated/hours_controller.rb b/app/controllers/api/v1/authenticated/hours_controller.rb new file mode 100644 index 0000000..d934881 --- /dev/null +++ b/app/controllers/api/v1/authenticated/hours_controller.rb @@ -0,0 +1,22 @@ +module Api + module V1 + module Authenticated + class HoursController < ApplicationController + def index + start_date = params[:start_date]&.to_date || 7.days.ago.to_date + end_date = params[:end_date]&.to_date || Date.current + + total_seconds = current_user.heartbeats + .where(created_at: start_date.beginning_of_day..end_date.end_of_day) + .duration_seconds + + render json: { + start_date: start_date, + end_date: end_date, + total_seconds: total_seconds + } + end + end + end + end +end diff --git a/app/controllers/api/v1/authenticated/projects_controller.rb b/app/controllers/api/v1/authenticated/projects_controller.rb new file mode 100644 index 0000000..782cb4f --- /dev/null +++ b/app/controllers/api/v1/authenticated/projects_controller.rb @@ -0,0 +1,47 @@ +module Api + module V1 + module Authenticated + class ProjectsController < ApplicationController + def index + projects = current_user.heartbeats + .where.not(project: [ nil, "" ]) + .group(:project) + .map { |project| + { + name: project, + total_seconds: time_per_project[project] || 0, + most_recent_heartbeat: most_recent_heartbeat_per_project[project] ? Time.at(most_recent_heartbeat_per_project[project]).strftime("%Y-%m-%dT%H:%M:%SZ") : nil, + percentage: time_per_project.sum { |_, secs| secs }.zero? ? 0 : ((time_per_project[project] || 0) / time_per_project.sum { |_, secs| secs }.to_f * 100).round(2), + repo: project_repo_mappings[project]&.repo + } + } + + render json: { projects: projects } + end + + private + + def project_repo_mappings + @project_repo_mappings ||= current_user.project_repo_mappings + .index_by(&:project) + end + + def time_per_project + @time_per_project ||= current_user.heartbeats + .with_valid_timestamps + .where.not(project: [ nil, "" ]) + .group(:project) + .duration_seconds + end + + def most_recent_heartbeat_per_project + @most_recent_heartbeat_per_project ||= current_user.heartbeats + .with_valid_timestamps + .where.not(project: [ nil, "" ]) + .group(:project) + .maximum(:time) + end + end + end + end +end diff --git a/app/controllers/api/v1/authenticated/streak_controller.rb b/app/controllers/api/v1/authenticated/streak_controller.rb new file mode 100644 index 0000000..5ec78cf --- /dev/null +++ b/app/controllers/api/v1/authenticated/streak_controller.rb @@ -0,0 +1,13 @@ +module Api + module V1 + module Authenticated + class StreakController < ApplicationController + def show + render json: { + streak_days: current_user.streak_days + } + end + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 3ad18af..c934c53 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -108,6 +108,16 @@ class User < ApplicationRecord has_many :trust_level_audit_logs, dependent: :destroy has_many :trust_level_changes_made, class_name: "TrustLevelAuditLog", foreign_key: "changed_by_id", dependent: :destroy + has_many :access_grants, + class_name: "Doorkeeper::AccessGrant", + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, + class_name: "Doorkeeper::AccessToken", + foreign_key: :resource_owner_id, + dependent: :delete_all + def streak_days @streak_days ||= heartbeats.daily_streaks_for_users([ id ]).values.first end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index e8546e2..60961f8 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -3,6 +3,10 @@ Doorkeeper.configure do base_controller "ApplicationController" + default_scopes "profile" + optional_scopes "read" + enforce_configured_scopes + resource_owner_authenticator do current_user || redirect_to(minimal_login_path(continue: request.fullpath)) end @@ -20,4 +24,11 @@ Doorkeeper.configure do access_token_expires_in 16.years reuse_access_token + + # Allow public clients (desktop/mobile apps) without client secrets + allow_blank_redirect_uri + skip_client_authentication_for_password_grant + + # Enable PKCE for public clients + force_ssl_in_redirect_uri false end diff --git a/config/routes.rb b/config/routes.rb index 377d8f4..97cf64c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,8 +154,14 @@ Rails.application.routes.draw do get "heartbeats", to: "heartbeats#index" end + # oauth authenticated namespace namespace :authenticated do resources :me, only: [ :index ] + get "hours", to: "hours#index" + get "streak", to: "streak#show" + get "projects", to: "projects#index" + # get "projects/:name", to: "projects#show", constraints: { name: /.+/ } + get "heartbeats/latest", to: "heartbeats#latest" end end diff --git a/db/migrate/20251003161836_add_foreign_keys_to_oauth_access.rb b/db/migrate/20251003161836_add_foreign_keys_to_oauth_access.rb new file mode 100644 index 0000000..3d7ee37 --- /dev/null +++ b/db/migrate/20251003161836_add_foreign_keys_to_oauth_access.rb @@ -0,0 +1,6 @@ +class AddForeignKeysToOauthAccess < ActiveRecord::Migration[8.0] + def change + add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id + add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id + end +end diff --git a/db/schema.rb b/db/schema.rb index db9627d..299b499 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_21_021751) do +ActiveRecord::Schema[8.0].define(version: 2025_10_03_161836) do create_schema "pganalyze" # These are extensions that must be enabled in order to support this database @@ -584,7 +584,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_21_021751) do add_foreign_key "leaderboard_entries", "users" add_foreign_key "mailing_addresses", "users" add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" add_foreign_key "physical_mails", "users" add_foreign_key "project_repo_mappings", "repositories" add_foreign_key "project_repo_mappings", "users" diff --git a/db/seeds.rb b/db/seeds.rb index 3061d06..11a31e7 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -2,6 +2,14 @@ # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +Doorkeeper::Application.find_or_create_by( + name: "Hackatime Desktop", + redirect_uri: "hackatime://auth/callback", + uid: "BPr5VekIV-xuQ2ZhmxbGaahJ3XVd7gM83pql-HYGYxQ", + scopes: [ "profile" ], + confidential: false, +) + # Only seed test data in development environment if Rails.env.development? # Creating test user