diff --git a/app/javascript/pages/Admin/LeaderboardShadowbans.svelte b/app/javascript/pages/Admin/LeaderboardShadowbans.svelte index bf144c04d..480a4c5b7 100644 --- a/app/javascript/pages/Admin/LeaderboardShadowbans.svelte +++ b/app/javascript/pages/Admin/LeaderboardShadowbans.svelte @@ -21,9 +21,7 @@ const redButtonClass = "!border-red !bg-red !text-on-primary hover:!opacity-90"; - type Pending = - | { kind: "ban" } - | { kind: "unban"; user: ShadowbannedUser }; + type Pending = { kind: "ban" } | { kind: "unban"; user: ShadowbannedUser }; let selectedUser = $state(null); let reason = $state(""); @@ -147,7 +145,10 @@ pending !== null, (v) => { if (!v) pending = null; } + () => pending !== null, + (v) => { + if (!v) pending = null; + } } title={pending?.kind === "unban" ? "Remove leaderboard shadowban?" @@ -179,9 +180,7 @@ {#if submitting} {pending?.kind === "unban" ? "Removing..." : "Saving..."} {:else} - {pending?.kind === "unban" - ? "Remove shadowban" - : "Confirm shadowban"} + {pending?.kind === "unban" ? "Remove shadowban" : "Confirm shadowban"} {/if} diff --git a/spec/requests/api/admin/v1/admin_timeline_spec.rb b/spec/requests/api/admin/v1/admin_timeline_spec.rb index e0a755e49..574ddf054 100644 --- a/spec/requests/api/admin/v1/admin_timeline_spec.rb +++ b/spec/requests/api/admin/v1/admin_timeline_spec.rb @@ -13,71 +13,6 @@ parameter name: :slack_uids, in: :query, type: :string, description: 'Comma-separated list of Slack User IDs' response(200, 'successful') do - schema type: :object, - properties: { - date: { type: :string, format: :date }, - next_date: { type: :string, format: :date }, - prev_date: { type: :string, format: :date }, - users: { - type: :array, - items: { - type: :object, - properties: { - user: { - type: :object, - properties: { - id: { type: :integer }, - username: { type: :string }, - display_name: { type: :string, nullable: true }, - slack_username: { type: :string, nullable: true }, - github_username: { type: :string, nullable: true }, - timezone: { type: :string, nullable: true }, - avatar_url: { type: :string, nullable: true } - } - }, - spans: { - type: :array, - items: { - type: :object, - properties: { - start_time: { type: :number, format: :float }, - end_time: { type: :number, format: :float }, - duration: { type: :number, format: :float }, - files_edited: { type: :array, items: { type: :string } }, - projects_edited_details: { - type: :array, - items: { - type: :object, - properties: { - name: { type: :string }, - repo_url: { type: :string, nullable: true } - } - } - }, - editors: { type: :array, items: { type: :string } }, - languages: { type: :array, items: { type: :string } } - } - } - }, - total_coded_time: { type: :number, format: :float } - } - } - }, - commit_markers: { - type: :array, - items: { - type: :object, - properties: { - user_id: { type: :integer }, - timestamp: { type: :number, format: :float }, - additions: { type: :integer, nullable: true }, - deletions: { type: :integer, nullable: true }, - github_url: { type: :string, nullable: true } - } - } - } - } - let(:Authorization) { "Bearer dev-admin-api-key-12345" } let(:date) { Time.current.to_date.to_s } let(:user_ids) { User.first&.id.to_s } diff --git a/spec/requests/api/admin/v1/admin_user_utils_spec.rb b/spec/requests/api/admin/v1/admin_user_utils_spec.rb index 19dbff670..7330c5476 100644 --- a/spec/requests/api/admin/v1/admin_user_utils_spec.rb +++ b/spec/requests/api/admin/v1/admin_user_utils_spec.rb @@ -143,8 +143,8 @@ properties: { id: { type: :integer }, time: { type: :number }, - lineno: { type: :integer, nullable: true }, - cursorpos: { type: :integer, nullable: true }, + lineno: { type: :integer }, + cursorpos: { type: :integer }, is_write: { type: :boolean, nullable: true }, project: { type: :string, nullable: true }, language: { type: :string, nullable: true }, @@ -326,87 +326,6 @@ run_test! end - response(200, 'successful') do - schema oneOf: [ - { - type: :object, - properties: { - segment: { type: :string }, - limit: { type: :integer }, - offset: { type: :integer }, - heartbeats: { - type: :array, - items: { - type: :object, - properties: { - id: { type: :integer }, - user_id: { type: :integer }, - time: { type: :number }, - project: { type: :string, nullable: true }, - language: { type: :string, nullable: true }, - entity: { type: :string, nullable: true }, - branch: { type: :string, nullable: true }, - category: { type: :string, nullable: true }, - editor: { type: :string, nullable: true }, - machine: { type: :string, nullable: true }, - operating_system: { type: :string, nullable: true }, - user_agent: { type: :string, nullable: true }, - ip_address: { type: :string, nullable: true }, - is_write: { type: :boolean, nullable: true }, - lineno: { type: :integer, nullable: true }, - cursorpos: { type: :integer, nullable: true }, - lines: { type: :integer, nullable: true }, - source_type: { type: :string, nullable: true } - } - } - }, - has_more: { type: :boolean } - } - }, - { - type: :object, - description: 'Returned when count_only=true', - additionalProperties: false, - required: [ 'segment', 'total_count' ], - properties: { - segment: { type: :string }, - total_count: { type: :integer } - } - } - ] - - let(:Authorization) { "Bearer dev-admin-api-key-12345" } - let(:hb_user_doc) do - u = User.create!(username: 'hb_segment_doc_user') - EmailAddress.create!(user: u, email: 'hb-segment-doc@example.com') - u - end - before do - Heartbeat.create!( - user: hb_user_doc, - time: Time.current.to_i, - project: 'demo', - language: 'GDScript', - editor: 'Godot', - source_type: :direct_entry, - branch: 'main', - category: 'coding', - is_write: true, - user_agent: 'Godot/4.2 Godot_Super-Wakatime/2.0.0', - operating_system: 'linux', - machine: 'test-machine' - ) - end - let(:segment) { 'Godot_Super-Wakatime' } - let(:user_id) { nil } - let(:start_date) { nil } - let(:end_date) { nil } - let(:limit) { 10 } - let(:offset) { 0 } - let(:count_only) { nil } - run_test! - end - response(422, 'missing segment') do let(:Authorization) { "Bearer dev-admin-api-key-12345" } let(:segment) { '' } @@ -569,7 +488,8 @@ properties: { user_id: { type: :integer }, username: { type: :string }, - date: { type: :string, format: :date_time }, + start_date: { type: :string, format: :date_time }, + end_date: { type: :string, format: :date_time }, timezone: { type: :string, nullable: true }, total_heartbeats: { type: :integer }, total_duration: { type: :number }, @@ -793,7 +713,9 @@ type: :object, properties: { user_id: { type: :integer }, - reason: { type: :string } + reason: { type: :string }, + trust_level: { type: :string }, + notes: { type: :string } } } diff --git a/spec/requests/api/admin/v1/admin_users_spec.rb b/spec/requests/api/admin/v1/admin_users_spec.rb index 4ae9d1ee9..e4d4d9272 100644 --- a/spec/requests/api/admin/v1/admin_users_spec.rb +++ b/spec/requests/api/admin/v1/admin_users_spec.rb @@ -1,43 +1,6 @@ require 'swagger_helper' RSpec.describe 'Api::Admin::V1::AdminUsers', type: :request do - path '/api/admin/v1/user/info' do - get('Get user info (Admin)') do - tags 'Admin' - description 'Get detailed info about a user. Requires superadmin/admin privileges.' - security [ AdminToken: [] ] - produces 'application/json' - - parameter name: :user_id, in: :query, type: :string, description: 'User ID' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-admin-api-key-12345" } - let(:user_id) { '1' } - let(:date) { '2023-01-01' } - run_test! - end - end - end - - path '/api/admin/v1/user/heartbeats' do - get('Get user heartbeats (Admin)') do - tags 'Admin' - description 'Get raw heartbeats for a user.' - security [ AdminToken: [] ] - produces 'application/json' - - parameter name: :user_id, in: :query, type: :string, description: 'User ID' - parameter name: :date, in: :query, schema: { type: :string, format: :date }, description: 'Date (YYYY-MM-DD)' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-admin-api-key-12345" } - let(:user_id) { '1' } - let(:date) { '2023-01-01' } - run_test! - end - end - end - path '/api/admin/v1/check' do get('Check status') do tags 'Admin' diff --git a/spec/requests/api/hackatime/v1/compatibility_spec.rb b/spec/requests/api/hackatime/v1/compatibility_spec.rb index 254f9740a..1905a2ceb 100644 --- a/spec/requests/api/hackatime/v1/compatibility_spec.rb +++ b/spec/requests/api/hackatime/v1/compatibility_spec.rb @@ -6,7 +6,7 @@ tags 'WakaTime Compatibility' description 'Endpoint used by WakaTime plugins to send heartbeat data to the server. This is the core endpoint for tracking time.' consumes 'application/json' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { ApiKeyAuth: [] } ] parameter name: :id, in: :path, type: :string, description: 'User ID or "current" (recommended)' parameter name: :heartbeats, in: :body, schema: { @@ -24,7 +24,17 @@ lineno: { type: :integer }, cursorpos: { type: :integer }, lines: { type: :integer }, - category: { type: :string } + category: { type: :string }, + created_at: { type: :string, format: :date_time }, + dependencies: { type: :array, items: { type: :string } }, + editor: { type: :string }, + line_additions: { type: :integer }, + line_deletions: { type: :integer }, + machine: { type: :string }, + operating_system: { type: :string }, + project_root_count: { type: :integer }, + user_agent: { type: :string }, + plugin: { type: :string } } } } @@ -72,7 +82,7 @@ get('Get status bar today') do tags 'WakaTime Compatibility' description 'Returns the total coding time for today. Used by editor plugins to display the status bar widget.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { ApiKeyAuth: [] } ] produces 'application/json' parameter name: :id, in: :path, type: :string, description: 'User ID or "current"' @@ -122,7 +132,7 @@ get('Get last 7 days stats') do tags 'WakaTime Compatibility' description 'Returns coding statistics for the last 7 days. Used by some WakaTime dashboards.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { ApiKeyAuth: [] } ] produces 'application/json' response(200, 'successful') do diff --git a/spec/requests/api/internal/internal_spec.rb b/spec/requests/api/internal/internal_spec.rb index ff128142e..511e6d87a 100644 --- a/spec/requests/api/internal/internal_spec.rb +++ b/spec/requests/api/internal/internal_spec.rb @@ -39,8 +39,8 @@ success: { type: :boolean }, status: { type: :string }, token_type: { type: :string }, - owner_email: { type: :string, nullable: true }, - key_name: { type: :string, nullable: true } + owner_email: { type: :string }, + key_name: { type: :string } } run_test! do |response| body = JSON.parse(response.body) diff --git a/spec/requests/api/summary_spec.rb b/spec/requests/api/summary_spec.rb index 81b5b4541..862ff2713 100644 --- a/spec/requests/api/summary_spec.rb +++ b/spec/requests/api/summary_spec.rb @@ -4,12 +4,14 @@ path '/api/summary' do get('Get WakaTime-compatible summary') do tags 'WakaTime Compatibility' - description 'Returns a summary of coding activity in a format compatible with WakaTime clients. This endpoint supports querying by date range, interval, or specific user (admin/privileged only).' - security [ Bearer: [], ApiKeyAuth: [] ] + description 'Returns a public summary of coding activity in a format compatible with WakaTime clients.' produces 'application/json' parameter name: :start, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD)' parameter name: :end, in: :query, schema: { type: :string, format: :date }, description: 'End date (YYYY-MM-DD)' + parameter name: :from, in: :query, schema: { type: :string, format: :date }, required: false, description: 'Alias for start (YYYY-MM-DD)' + parameter name: :to, in: :query, schema: { type: :string, format: :date }, required: false, description: 'Alias for end (YYYY-MM-DD)' + parameter name: :range, in: :query, type: :string, required: false, description: 'Predefined range (e.g. today, yesterday, week, month). Alias for interval.' parameter name: :interval, in: :query, type: :string, description: 'Interval (e.g. today, yesterday, week, month)' parameter name: :project, in: :query, type: :string, description: 'Project name (optional)' parameter name: :user_id, in: :query, type: :string, required: true, description: 'User identifier (slack_uid, username, hca_id, or numeric ID)' @@ -22,6 +24,9 @@ let(:start) { '2023-01-01' } let(:end) { '2023-01-31' } let(:interval) { nil } + let(:from) { nil } + let(:to) { nil } + let(:range) { nil } let(:project) { nil } let(:user_id) { test_user.slack_uid } let(:user) { nil } @@ -67,6 +72,9 @@ let(:start) { '2023-01-01' } let(:end) { '2023-01-31' } let(:interval) { nil } + let(:from) { nil } + let(:to) { nil } + let(:range) { nil } let(:project) { nil } let(:user_id) { nil } let(:user) { nil } @@ -80,6 +88,9 @@ let(:start) { 'invalid-date' } let(:end) { '2023-01-31' } let(:interval) { nil } + let(:from) { nil } + let(:to) { nil } + let(:range) { nil } let(:project) { nil } let(:user_id) { date_test_user.slack_uid } let(:user) { nil } @@ -92,6 +103,9 @@ let(:start) { '2023-01-01' } let(:end) { '2023-01-31' } let(:interval) { nil } + let(:from) { nil } + let(:to) { nil } + let(:range) { nil } let(:project) { nil } let(:user_id) { 'nonexistent-user-id' } let(:user) { nil } @@ -105,6 +119,9 @@ let(:start) { '2023-01-01' } let(:end) { '2023-01-31' } let(:interval) { nil } + let(:from) { nil } + let(:to) { nil } + let(:range) { nil } let(:project) { nil } let(:user_id) { private_user.slack_uid } let(:user) { nil } diff --git a/spec/requests/api/v1/authenticated_spec.rb b/spec/requests/api/v1/authenticated_spec.rb index 46af3db71..94596a93f 100644 --- a/spec/requests/api/v1/authenticated_spec.rb +++ b/spec/requests/api/v1/authenticated_spec.rb @@ -5,7 +5,7 @@ get('Get current user info') do tags 'Authenticated' description 'Returns detailed information about the currently authenticated user.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' response(200, 'successful') do @@ -45,7 +45,7 @@ get('Get hours') do tags 'Authenticated' description 'Returns the total coding hours for the authenticated user.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD)' @@ -80,7 +80,7 @@ get('Get streak') do tags 'Authenticated' description 'Returns the current streak information (days coded in a row).' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' response(200, 'successful') do @@ -108,7 +108,7 @@ get('Get projects') do tags 'Authenticated' description 'Returns a list of projects associated with the authenticated user.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' parameter name: :include_archived, in: :query, type: :boolean, description: 'Include archived projects (true/false)' @@ -174,7 +174,7 @@ get('Get API keys') do tags 'Authenticated' description 'Returns the API keys for the authenticated user. Warning: This returns sensitive information.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' response(200, 'successful') do @@ -200,7 +200,7 @@ get('Get latest heartbeat') do tags 'Authenticated' description 'Returns the absolutely latest heartbeat processed for the user.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' response(200, 'successful') do diff --git a/spec/requests/api/v1/leaderboards_spec.rb b/spec/requests/api/v1/leaderboards_spec.rb index cb103a000..082c8f270 100644 --- a/spec/requests/api/v1/leaderboards_spec.rb +++ b/spec/requests/api/v1/leaderboards_spec.rb @@ -14,7 +14,6 @@ get('Get daily leaderboard (Alias)') do tags 'Leaderboard' description 'Alias for /api/v1/leaderboard/daily. Returns the daily leaderboard.' - security [ Bearer: [], ApiKeyAuth: [] ] produces 'application/json' response(200, 'successful') do @@ -48,8 +47,7 @@ path '/api/v1/leaderboard/daily' do get('Get daily leaderboard') do tags 'Leaderboard' - description 'Returns the daily leaderboard of coding time. Requires STATS_API_KEY. The leaderboard is cached and regenerated periodically.' - security [ Bearer: [], ApiKeyAuth: [] ] + description 'Returns the daily leaderboard of coding time. The leaderboard is cached and regenerated periodically.' produces 'application/json' response(200, 'successful') do @@ -95,8 +93,7 @@ path '/api/v1/leaderboard/weekly' do get('Get weekly leaderboard') do tags 'Leaderboard' - description 'Returns the weekly leaderboard of coding time (last 7 days). Requires STATS_API_KEY.' - security [ Bearer: [], ApiKeyAuth: [] ] + description 'Returns the weekly leaderboard of coding time (last 7 days).' produces 'application/json' response(200, 'successful') do diff --git a/spec/requests/api/v1/my_spec.rb b/spec/requests/api/v1/my_spec.rb index 9e823d631..747a25918 100644 --- a/spec/requests/api/v1/my_spec.rb +++ b/spec/requests/api/v1/my_spec.rb @@ -20,7 +20,7 @@ def login_browser_user get('Get most recent heartbeat') do tags 'My Data' description 'Returns the most recent heartbeat for the authenticated user. Useful for checking if the user is currently active.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { BasicApiKey: [] } ] produces 'application/json' parameter name: :source_type, in: :query, type: :string, description: 'Filter by source type (e.g. "direct_entry")' @@ -49,7 +49,7 @@ def login_browser_user get('Get heartbeats') do tags 'My Data' description 'Returns a list of heartbeats for the authenticated user within a time range. This is the raw data stream.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { BasicApiKey: [] } ] produces 'application/json' parameter name: :start_time, in: :query, schema: { type: :string, format: :date_time }, description: 'Start time (ISO 8601)' @@ -73,245 +73,4 @@ def login_browser_user end end end - - path '/my/heartbeats/export' do - post('Export Heartbeats') do - tags 'My Data' - description 'Export your heartbeats as a JSON file.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :all_data, in: :query, type: :boolean, description: 'Export all data (true/false)' - parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD)' - parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, description: 'End date (YYYY-MM-DD)' - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:all_data) { true } - let(:start_date) { Date.today.to_s } - let(:end_date) { Date.today.to_s } - - before { login_browser_user } - run_test! - end - end - end - - path '/my/heartbeat_imports' do - post('Create Heartbeat Import') do - tags 'My Data' - description 'Start a development upload import or a one-time remote dump import.' - security [ Bearer: [], ApiKeyAuth: [] ] - consumes 'multipart/form-data' - produces 'application/json' - - parameter name: :"heartbeat_import[provider]", - in: :formData, - schema: { type: :string, enum: %w[wakatime_dump hackatime_v1_dump] }, - description: 'Remote import provider preset' - parameter name: :"heartbeat_import[api_key]", - in: :formData, - schema: { type: :string }, - description: 'API key for the selected remote import provider' - - response(202, 'accepted') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:"heartbeat_import[provider]") { "wakatime_dump" } - let(:"heartbeat_import[api_key]") { "test-api-key" } - - before do - login_browser_user - Flipper.enable_actor(:imports, user) - end - after { Flipper.disable(:imports) } - run_test! - end - end - end - - path '/my/projects' do - get('List Project Repo Mappings') do - tags 'My Projects' - description 'List mappings between local project names and Git repositories.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'text/html' - - parameter name: :interval, in: :query, type: :string, description: 'Time interval (e.g., daily, weekly). Default: daily' - parameter name: :from, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD)' - parameter name: :to, in: :query, schema: { type: :string, format: :date }, description: 'End date (YYYY-MM-DD)' - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:interval) { 'daily' } - let(:from) { Date.today.to_s } - let(:to) { Date.today.to_s } - - before { login_browser_user } - run_test! - end - end - end - - path '/my/project_repo_mappings/{project_name}' do - parameter name: :project_name, in: :path, type: :string, description: 'Project name (encoded)' - - patch('Update Project Repo Mapping') do - tags 'My Projects' - description 'Update the Git repository URL for a project mapping.' - security [ Bearer: [], ApiKeyAuth: [] ] - consumes 'application/json' - produces 'application/json' - - parameter name: :project_repo_mapping, in: :body, schema: { - type: :object, - properties: { - repo_url: { type: :string, example: 'https://github.com/hackclub/hackatime' } - }, - required: [ 'repo_url' ] - } - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:project_name) { 'hackatime' } - let(:project_repo_mapping) { { repo_url: 'https://github.com/hackclub/hackatime' } } - - before do - login_browser_user - user.update(github_uid: '12345') - end - - run_test! - end - end - end - - path '/my/project_repo_mappings/{project_name}/archive' do - parameter name: :project_name, in: :path, type: :string, description: 'Project name (encoded)' - - patch('Archive Project Mapping') do - tags 'My Projects' - description 'Archive a project mapping so it does not appear in active lists.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:project_name) { 'hackatime' } - - before do - login_browser_user - user.project_repo_mappings.create!(project_name: 'hackatime') - end - run_test! - end - end - end - - path '/my/project_repo_mappings/{project_name}/unarchive' do - parameter name: :project_name, in: :path, type: :string, description: 'Project name (encoded)' - - patch('Unarchive Project Mapping') do - tags 'My Projects' - description 'Restore an archived project mapping.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:project_name) { 'hackatime' } - - before do - login_browser_user - p = user.project_repo_mappings.create!(project_name: 'hackatime') - p.archive! - end - run_test! - end - end - end - - path '/my/settings/rotate_api_key' do - post('Rotate API Key') do - tags 'My Settings' - description 'Rotate your API key. Returns the new token. Warning: Old token will stop working immediately.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - - before { login_browser_user } - run_test! - end - end - end - - path '/my/heartbeat_imports/{id}' do - get('Get Heartbeat Import Status') do - tags 'My Data' - description 'Fetch the latest state for a heartbeat import run.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :id, in: :path, type: :string, description: 'Heartbeat import run id' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - let(:id) do - HeartbeatImportRun.create!( - user: user, - source_kind: :dev_upload, - state: :completed, - source_filename: "heartbeats.json", - message: "Completed." - ).id - end - - before { login_browser_user } - run_test! - end - end - end - - path '/deletion' do - post('Create Deletion Request') do - tags 'My Settings' - description 'Request deletion of your account and data.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'text/html' - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - - before { login_browser_user } - run_test! - end - end - - delete('Cancel Deletion Request') do - tags 'My Settings' - description 'Cancel a pending deletion request.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'text/html' - - response(302, 'redirect') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { 'dev-api-key-12345' } - - before do - login_browser_user - DeletionRequest.create_for_user!(user) - end - run_test! - end - end - end end diff --git a/spec/requests/api/v1/public_spec.rb b/spec/requests/api/v1/public_spec.rb new file mode 100644 index 000000000..5367ad838 --- /dev/null +++ b/spec/requests/api/v1/public_spec.rb @@ -0,0 +1,96 @@ +require 'swagger_helper' + +RSpec.describe 'Api::V1::Public', type: :request do + path '/api/v1/currently_hacking' do + get('List users currently hacking') do + tags 'Public' + description 'Returns users with recent coding activity.' + produces 'application/json' + + response(200, 'successful') do + schema type: :object, + properties: { + count: { type: :integer }, + users: { + type: :array, + items: { + type: :object, + properties: { + display_name: { type: :string, nullable: true }, + avatar_url: { type: :string, nullable: true }, + country_code: { type: :string, nullable: true }, + working_on: { + type: :object, + nullable: true, + properties: { + project_name: { type: :string }, + repo_url: { type: :string, nullable: true } + } + } + } + } + } + } + + run_test! + end + end + end + + path '/api/v1/banned_users/counts' do + get('Get banned user counts') do + tags 'Stats' + description 'Returns distinct red trust-level user counts over the last day, week, and month.' + produces 'application/json' + + response(200, 'successful') do + schema type: :object, + properties: { + day: { type: :integer }, + week: { type: :integer }, + month: { type: :integer } + } + + run_test! + end + end + end + + path '/api/v1/badge/{user_id}/{project}' do + get('Get project coding-time badge') do + tags 'Badges' + description 'Redirects to a shields.io badge for a user project.' + produces 'image/svg+xml' + + parameter name: :user_id, in: :path, type: :string, description: 'Slack UID, username, or internal user ID' + parameter name: :project, in: :path, type: :string, description: 'Project name or owner/repo' + parameter name: :label, in: :query, type: :string, required: false, description: 'Badge label' + parameter name: :color, in: :query, type: :string, required: false, description: 'Badge color' + parameter name: :aliases, in: :query, type: :string, required: false, description: 'Comma-separated extra project names to include' + + response(307, 'temporary redirect') do + let(:badge_user) { User.create!(username: "badge_user_#{SecureRandom.hex(4)}", allow_public_stats_lookup: true) } + let(:user_id) { badge_user.username } + let(:project) { 'hackatime' } + let(:label) { nil } + let(:color) { nil } + let(:aliases) { nil } + + before do + now = Time.current.to_f + [ now, now + 60 ].each do |time| + Heartbeat.create!( + user: badge_user, + time: time, + project: project, + category: 'coding', + source_type: :direct_entry + ) + end + end + + run_test! + end + end + end +end diff --git a/spec/requests/api/v1/stats_spec.rb b/spec/requests/api/v1/stats_spec.rb index 50a8ab5cb..d73feb0e4 100644 --- a/spec/requests/api/v1/stats_spec.rb +++ b/spec/requests/api/v1/stats_spec.rb @@ -5,7 +5,7 @@ get('Get total coding time (Admin Only)') do tags 'Stats' description 'Returns the total coding time for all users, optionally filtered by user or date range. Requires admin privileges via STATS_API_KEY.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ { Bearer: [] }, { ApiKeyAuth: [] } ] produces 'text/plain' parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD), defaults to 10 years ago' @@ -248,7 +248,6 @@ get('Get user stats') do tags 'Stats' description 'Returns detailed coding stats for a specific user, including languages, projects, and total time.' - security [ Bearer: [], ApiKeyAuth: [] ] produces 'application/json' parameter name: :username, in: :path, type: :string, description: 'Username, Slack ID, or User ID' diff --git a/spec/requests/api/v1/users_spec.rb b/spec/requests/api/v1/users_spec.rb index e9ec2f1d6..fc576ea39 100644 --- a/spec/requests/api/v1/users_spec.rb +++ b/spec/requests/api/v1/users_spec.rb @@ -5,7 +5,7 @@ get('Lookup user by email') do tags 'Users' description 'Find a user ID by their email address. Useful for integrations that need to map emails to Hackatime users. Requires STATS_API_KEY.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' parameter name: :email, in: :path, type: :string, description: 'Email address to lookup' @@ -47,7 +47,7 @@ get('Lookup user by Slack UID') do tags 'Users' description 'Find a user ID by their Slack User ID. Requires STATS_API_KEY.' - security [ Bearer: [], ApiKeyAuth: [] ] + security [ Bearer: [] ] produces 'application/json' parameter name: :slack_uid, in: :path, type: :string, description: 'Slack User ID (e.g. U123456)' @@ -79,156 +79,4 @@ end end end - - path '/api/v1/users/{username}/trust_factor' do - get('Get trust factor') do - tags 'Users' - description 'Get the trust level/factor for a user. Higher trust values indicate more verified activity.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :username, in: :path, type: :string, description: 'Username, Slack ID, or User ID' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'testuser' } - schema type: :object, - properties: { - trust_level: { type: :string, example: 'verified' }, - trust_value: { type: :integer, example: 2 } - } - run_test! - end - - response(404, 'not found') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'unknown_user' } - schema '$ref' => '#/components/schemas/Error' - run_test! - end - end - end - - path '/api/v1/users/{username}/projects' do - get('Get user projects') do - tags 'Users' - description 'Get a list of projects a user has coded on recently (last 30 days).' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :username, in: :path, type: :string, description: 'Username' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'testuser' } - schema type: :object, - properties: { - projects: { - type: :array, - items: { type: :string, example: 'hackatime' } - } - } - run_test! - end - - response(404, 'not found') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'unknown_user' } - schema '$ref' => '#/components/schemas/Error' - run_test! - end - end - end - - path '/api/v1/users/{username}/project/{project_name}' do - get('Get specific project stats') do - tags 'Users' - description 'Get detailed stats for a specific project.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :username, in: :path, type: :string, description: 'Username' - parameter name: :project_name, in: :path, type: :string, description: 'Project Name' - parameter name: :start, in: :query, schema: { type: :string, format: :date_time }, description: 'Stats start time (ISO 8601)' - parameter name: :end, in: :query, schema: { type: :string, format: :date_time }, description: 'Stats end time (ISO 8601)' - parameter name: :start_date, in: :query, schema: { type: :string, format: :date_time }, description: 'Start date (ISO 8601) for stats calculation' - parameter name: :end_date, in: :query, schema: { type: :string, format: :date_time }, description: 'End date (ISO 8601) for stats calculation' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'testuser' } - let(:project_name) { 'harbor' } - let(:start) { nil } - let(:end) { nil } - let(:start_date) { nil } - let(:end_date) { nil } - run_test! - end - end - end - - path '/api/v1/users/{username}/projects/details' do - get('Get detailed project stats') do - tags 'Users' - description 'Get detailed breakdown of all user projects.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :username, in: :path, type: :string, description: 'Username' - parameter name: :projects, in: :query, type: :string, description: 'Comma-separated list of projects to filter' - parameter name: :since, in: :query, schema: { type: :string, format: :date_time }, description: 'Start time (ISO 8601) for project discovery' - parameter name: :until, in: :query, schema: { type: :string, format: :date_time }, description: 'End time (ISO 8601) for project discovery' - parameter name: :until_date, in: :query, schema: { type: :string, format: :date_time }, description: 'End time (ISO 8601) for project discovery' - parameter name: :start, in: :query, schema: { type: :string, format: :date_time }, description: 'Stats start time (ISO 8601)' - parameter name: :end, in: :query, schema: { type: :string, format: :date_time }, description: 'Stats end time (ISO 8601)' - parameter name: :start_date, in: :query, schema: { type: :string, format: :date_time }, description: 'Start date (ISO 8601) for stats calculation' - parameter name: :end_date, in: :query, schema: { type: :string, format: :date_time }, description: 'End date (ISO 8601) for stats calculation' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'testuser' } - let(:projects) { nil } - let(:since) { nil } - let(:until) { nil } - let(:until_date) { nil } - let(:start) { nil } - let(:end) { nil } - let(:start_date) { nil } - let(:end_date) { nil } - run_test! - end - end - end - - path '/api/v1/users/{username}/heartbeats/spans' do - get('Get heartbeat spans') do - tags 'Users' - description 'Get time spans of coding activity.' - security [ Bearer: [], ApiKeyAuth: [] ] - produces 'application/json' - - parameter name: :username, in: :path, type: :string, description: 'Username' - parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, description: 'Start date (YYYY-MM-DD)' - parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, description: 'End date (YYYY-MM-DD)' - parameter name: :project, in: :query, type: :string, description: 'Filter by specific project' - parameter name: :filter_by_project, in: :query, type: :string, description: 'Filter by multiple projects (comma-separated)' - - response(200, 'successful') do - let(:Authorization) { "Bearer dev-api-key-12345" } - let(:api_key) { "dev-api-key-12345" } - let(:username) { 'testuser' } - let(:start_date) { Date.today.to_s } - let(:end_date) { Date.today.to_s } - let(:project) { nil } - let(:filter_by_project) { nil } - run_test! - end - end - end end diff --git a/spec/requests/slack_spec.rb b/spec/requests/slack_spec.rb deleted file mode 100644 index 90acf47dd..000000000 --- a/spec/requests/slack_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'swagger_helper' - -RSpec.describe 'Slack Webhooks', type: :request do - path '/sailors_log/slack/commands' do - post('Handle Sailor\'s Log Command') do - tags 'Slack' - description 'Handle incoming Slack slash commands for Sailor\'s Log (/sailorslog).' - consumes 'application/x-www-form-urlencoded' - produces 'application/json' - - parameter name: :command, in: :formData, type: :string - parameter name: :text, in: :formData, type: :string - parameter name: :user_id, in: :formData, type: :string - parameter name: :response_url, in: :formData, type: :string - - response(200, 'successful') do - let(:command) { '/sailorslog' } - let(:text) { 'status update' } - let(:user_id) { 'U123456' } - let(:response_url) { 'https://hooks.slack.com/commands/1234/5678' } - before { allow(Rails.env).to receive(:development?).and_return(true) } - schema type: :object, - properties: { - response_type: { type: :string }, - text: { type: :string, nullable: true }, - blocks: { - type: :array, - items: { type: :object } - } - } - run_test! - end - end - end -end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index f419af549..eb577fbf2 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -23,7 +23,12 @@ Bearer: { type: :http, scheme: :bearer, - description: 'User API Key from settings, prefixed with "Bearer"' + description: 'Bearer token accepted by the endpoint' + }, + BasicApiKey: { + type: :http, + scheme: :basic, + description: 'User API key as the Basic auth credential' }, AdminToken: { type: :http, diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index d23095ec8..4baaa1fa7 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -777,10 +777,10 @@ paths: nullable: true "/api/admin/v1/user/info": get: - summary: Get user info (Admin) + summary: Get user info tags: - - Admin - description: Get detailed info about a user. Requires superadmin/admin privileges. + - Admin Utils + description: Get detailed info for a single user. security: - AdminToken: [] parameters: @@ -859,10 +859,10 @@ paths: type: integer "/api/admin/v1/user/heartbeats": get: - summary: Get user heartbeats (Admin) + summary: Get admin user heartbeats tags: - - Admin - description: Get raw heartbeats for a user. + - Admin Utils + description: Get heartbeats for a user (Admin view). security: - AdminToken: [] parameters: @@ -871,12 +871,51 @@ paths: description: User ID schema: type: string - - name: date + - name: start_date in: query + description: Start date (YYYY-MM-DD or timestamp) schema: type: string - format: date - description: Date (YYYY-MM-DD) + - name: end_date + in: query + description: End date (YYYY-MM-DD or timestamp) + schema: + type: string + - name: project + in: query + description: Project name + schema: + type: string + - name: language + in: query + description: Language + schema: + type: string + - name: entity + in: query + description: Entity (file path or app name) + schema: + type: string + - name: editor + in: query + description: Editor + schema: + type: string + - name: machine + in: query + description: Machine + schema: + type: string + - name: limit + in: query + description: Limit + schema: + type: integer + - name: offset + in: query + description: Offset + schema: + type: integer responses: '200': description: successful @@ -898,10 +937,8 @@ paths: type: number lineno: type: integer - nullable: true cursorpos: type: integer - nullable: true is_write: type: boolean nullable: true @@ -1007,88 +1044,7 @@ paths: type: boolean responses: '200': - description: successful - content: - application/json: - schema: - oneOf: - - type: object - properties: - segment: - type: string - limit: - type: integer - offset: - type: integer - heartbeats: - type: array - items: - type: object - properties: - id: - type: integer - user_id: - type: integer - time: - type: number - project: - type: string - nullable: true - language: - type: string - nullable: true - entity: - type: string - nullable: true - branch: - type: string - nullable: true - category: - type: string - nullable: true - editor: - type: string - nullable: true - machine: - type: string - nullable: true - operating_system: - type: string - nullable: true - user_agent: - type: string - nullable: true - ip_address: - type: string - nullable: true - is_write: - type: boolean - nullable: true - lineno: - type: integer - nullable: true - cursorpos: - type: integer - nullable: true - lines: - type: integer - nullable: true - source_type: - type: string - nullable: true - has_more: - type: boolean - - type: object - description: Returned when count_only=true - additionalProperties: false - required: - - segment - - total_count - properties: - segment: - type: string - total_count: - type: integer + description: count_only returns total only '422': description: missing segment content: @@ -1249,7 +1205,10 @@ paths: type: integer username: type: string - date: + start_date: + type: string + format: date_time + end_date: type: string format: date_time timezone: @@ -1583,6 +1542,10 @@ paths: type: integer reason: type: string + trust_level: + type: string + notes: + type: string "/api/admin/v1/check": get: summary: Check status @@ -1849,7 +1812,7 @@ paths: server. This is the core endpoint for tracking time. security: - Bearer: [] - ApiKeyAuth: [] + - ApiKeyAuth: [] parameters: - name: id in: path @@ -1892,6 +1855,29 @@ paths: type: integer category: type: string + created_at: + type: string + format: date_time + dependencies: + type: array + items: + type: string + editor: + type: string + line_additions: + type: integer + line_deletions: + type: integer + machine: + type: string + operating_system: + type: string + project_root_count: + type: integer + user_agent: + type: string + plugin: + type: string "/api/hackatime/v1/users/{id}/statusbar/today": get: summary: Get status bar today @@ -1901,7 +1887,7 @@ paths: to display the status bar widget. security: - Bearer: [] - ApiKeyAuth: [] + - ApiKeyAuth: [] parameters: - name: id in: path @@ -1956,7 +1942,7 @@ paths: dashboards. security: - Bearer: [] - ApiKeyAuth: [] + - ApiKeyAuth: [] responses: '200': description: successful @@ -2153,10 +2139,8 @@ paths: type: string owner_email: type: string - nullable: true key_name: type: string - nullable: true '422': description: unprocessable entity content: @@ -2190,12 +2174,8 @@ paths: summary: Get WakaTime-compatible summary tags: - WakaTime Compatibility - description: Returns a summary of coding activity in a format compatible with - WakaTime clients. This endpoint supports querying by date range, interval, - or specific user (admin/privileged only). - security: - - Bearer: [] - ApiKeyAuth: [] + description: Returns a public summary of coding activity in a format compatible + with WakaTime clients. parameters: - name: start in: query @@ -2209,6 +2189,27 @@ paths: type: string format: date description: End date (YYYY-MM-DD) + - name: from + in: query + schema: + type: string + format: date + required: false + description: Alias for start (YYYY-MM-DD) + - name: to + in: query + schema: + type: string + format: date + required: false + description: Alias for end (YYYY-MM-DD) + - name: range + in: query + required: false + description: Predefined range (e.g. today, yesterday, week, month). Alias + for interval. + schema: + type: string - name: interval in: query description: Interval (e.g. today, yesterday, week, month) @@ -2301,7 +2302,6 @@ paths: user. security: - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2339,7 +2339,6 @@ paths: description: Returns the total coding hours for the authenticated user. security: - Bearer: [] - ApiKeyAuth: [] parameters: - name: start_date in: query @@ -2382,7 +2381,6 @@ paths: description: Returns the current streak information (days coded in a row). security: - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2404,7 +2402,6 @@ paths: description: Returns a list of projects associated with the authenticated user. security: - Bearer: [] - ApiKeyAuth: [] parameters: - name: include_archived in: query @@ -2498,7 +2495,6 @@ paths: returns sensitive information.' security: - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2520,7 +2516,6 @@ paths: description: Returns the absolutely latest heartbeat processed for the user. security: - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2595,9 +2590,6 @@ paths: tags: - Leaderboard description: Alias for /api/v1/leaderboard/daily. Returns the daily leaderboard. - security: - - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2630,9 +2622,6 @@ paths: tags: - Leaderboard description: Leaderboard is currently being generated - security: - - Bearer: [] - ApiKeyAuth: [] responses: '200': description: successful @@ -2670,11 +2659,7 @@ paths: summary: Get weekly leaderboard tags: - Leaderboard - description: Returns the weekly leaderboard of coding time (last 7 days). Requires - STATS_API_KEY. - security: - - Bearer: [] - ApiKeyAuth: [] + description: Returns the weekly leaderboard of coding time (last 7 days). responses: '200': description: successful @@ -2710,7 +2695,7 @@ paths: for checking if the user is currently active. security: - Bearer: [] - ApiKeyAuth: [] + - BasicApiKey: [] parameters: - name: source_type in: query @@ -2736,7 +2721,7 @@ paths: a time range. This is the raw data stream. security: - Bearer: [] - ApiKeyAuth: [] + - BasicApiKey: [] parameters: - name: start_time in: query @@ -2755,212 +2740,106 @@ paths: description: successful '401': description: unauthorized - "/my/heartbeats/export": - post: - summary: Export Heartbeats + "/api/v1/currently_hacking": + get: + summary: List users currently hacking tags: - - My Data - description: Export your heartbeats as a JSON file. - security: - - Bearer: [] - ApiKeyAuth: [] - parameters: - - name: all_data - in: query - description: Export all data (true/false) - schema: - type: boolean - - name: start_date - in: query - schema: - type: string - format: date - description: Start date (YYYY-MM-DD) - - name: end_date - in: query - schema: - type: string - format: date - description: End date (YYYY-MM-DD) + - Public + description: Returns users with recent coding activity. responses: - '302': - description: redirect - "/my/heartbeat_imports": - post: - summary: Create Heartbeat Import + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + count: + type: integer + users: + type: array + items: + type: object + properties: + display_name: + type: string + nullable: true + avatar_url: + type: string + nullable: true + country_code: + type: string + nullable: true + working_on: + type: object + nullable: true + properties: + project_name: + type: string + repo_url: + type: string + nullable: true + "/api/v1/banned_users/counts": + get: + summary: Get banned user counts tags: - - My Data - description: Start a development upload import or a one-time remote dump import. - security: - - Bearer: [] - ApiKeyAuth: [] - parameters: [] + - Stats + description: Returns distinct red trust-level user counts over the last day, + week, and month. responses: - '202': - description: accepted - requestBody: - content: - multipart/form-data: - schema: - type: string - enum: - - wakatime_dump - - hackatime_v1_dump - description: Remote import provider preset - "/my/projects": + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + day: + type: integer + week: + type: integer + month: + type: integer + "/api/v1/badge/{user_id}/{project}": get: - summary: List Project Repo Mappings + summary: Get project coding-time badge tags: - - My Projects - description: List mappings between local project names and Git repositories. - security: - - Bearer: [] - ApiKeyAuth: [] + - Badges + description: Redirects to a shields.io badge for a user project. parameters: - - name: interval - in: query - description: 'Time interval (e.g., daily, weekly). Default: daily' + - name: user_id + in: path + description: Slack UID, username, or internal user ID + required: true schema: type: string - - name: from + - name: project + in: path + description: Project name or owner/repo + required: true + schema: + type: string + - name: label in: query + required: false + description: Badge label schema: type: string - format: date - description: Start date (YYYY-MM-DD) - - name: to + - name: color in: query + required: false + description: Badge color schema: type: string - format: date - description: End date (YYYY-MM-DD) - responses: - '200': - description: successful - "/my/project_repo_mappings/{project_name}": - parameters: - - name: project_name - in: path - description: Project name (encoded) - required: true - schema: - type: string - patch: - summary: Update Project Repo Mapping - tags: - - My Projects - description: Update the Git repository URL for a project mapping. - security: - - Bearer: [] - ApiKeyAuth: [] - parameters: [] - responses: - '302': - description: redirect - requestBody: - content: - application/json: - schema: - type: object - properties: - repo_url: - type: string - example: https://github.com/hackclub/hackatime - required: - - repo_url - "/my/project_repo_mappings/{project_name}/archive": - parameters: - - name: project_name - in: path - description: Project name (encoded) - required: true - schema: - type: string - patch: - summary: Archive Project Mapping - tags: - - My Projects - description: Archive a project mapping so it does not appear in active lists. - security: - - Bearer: [] - ApiKeyAuth: [] - responses: - '302': - description: redirect - "/my/project_repo_mappings/{project_name}/unarchive": - parameters: - - name: project_name - in: path - description: Project name (encoded) - required: true - schema: - type: string - patch: - summary: Unarchive Project Mapping - tags: - - My Projects - description: Restore an archived project mapping. - security: - - Bearer: [] - ApiKeyAuth: [] - responses: - '302': - description: redirect - "/my/settings/rotate_api_key": - post: - summary: Rotate API Key - tags: - - My Settings - description: 'Rotate your API key. Returns the new token. Warning: Old token - will stop working immediately.' - security: - - Bearer: [] - ApiKeyAuth: [] - responses: - '200': - description: successful - "/my/heartbeat_imports/{id}": - get: - summary: Get Heartbeat Import Status - tags: - - My Data - description: Fetch the latest state for a heartbeat import run. - security: - - Bearer: [] - ApiKeyAuth: [] - parameters: - - name: id - in: path - description: Heartbeat import run id - required: true + - name: aliases + in: query + required: false + description: Comma-separated extra project names to include schema: type: string responses: - '200': - description: successful - "/deletion": - post: - summary: Create Deletion Request - tags: - - My Settings - description: Request deletion of your account and data. - security: - - Bearer: [] - ApiKeyAuth: [] - responses: - '302': - description: redirect - delete: - summary: Cancel Deletion Request - tags: - - My Settings - description: Cancel a pending deletion request. - security: - - Bearer: [] - ApiKeyAuth: [] - responses: - '302': - description: redirect + '307': + description: temporary redirect "/api/v1/stats": get: summary: Get total coding time (Admin Only) @@ -2970,7 +2849,7 @@ paths: by user or date range. Requires admin privileges via STATS_API_KEY. security: - Bearer: [] - ApiKeyAuth: [] + - ApiKeyAuth: [] parameters: - name: start_date in: query @@ -3016,14 +2895,13 @@ paths: "$ref": "#/components/schemas/Error" "/api/v1/users/{username}/heartbeats/spans": get: - summary: Get heartbeat spans + summary: Get user heartbeat spans tags: - - Users - description: Get time spans of coding activity. + - Stats + description: Returns heartbeat spans for a user, useful for visualizations. parameters: - name: username in: path - description: Username required: true schema: type: string @@ -3032,21 +2910,19 @@ paths: schema: type: string format: date - description: Start date (YYYY-MM-DD) - name: end_date in: query schema: type: string format: date - description: End date (YYYY-MM-DD) - name: project in: query - description: Filter by specific project + description: Filter by single project schema: type: string - name: filter_by_project in: query - description: Filter by multiple projects (comma-separated) + description: Filter by multiple projects (comma separated) schema: type: string responses: @@ -3074,20 +2950,15 @@ paths: application/json: schema: "$ref": "#/components/schemas/Error" - security: - - Bearer: [] - ApiKeyAuth: [] "/api/v1/users/{username}/trust_factor": get: - summary: Get trust factor + summary: Get user trust factor tags: - - Users - description: Get the trust level/factor for a user. Higher trust values indicate - more verified activity. + - Stats + description: Returns the trust level and value for a user. parameters: - name: username in: path - description: Username, Slack ID, or User ID required: true schema: type: string @@ -3101,29 +2972,19 @@ paths: properties: trust_level: type: string - example: verified + example: blue trust_value: type: integer - example: 2 - '404': - description: not found - content: - application/json: - schema: - "$ref": "#/components/schemas/Error" - security: - - Bearer: [] - ApiKeyAuth: [] + example: 3 "/api/v1/users/{username}/projects": get: - summary: Get user projects + summary: Get user project names tags: - - Users - description: Get a list of projects a user has coded on recently (last 30 days). + - Stats + description: Returns a list of project names for a user from the last 30 days. parameters: - name: username in: path - description: Username required: true schema: type: string @@ -3139,32 +3000,20 @@ paths: type: array items: type: string - example: hackatime - '404': - description: not found - content: - application/json: - schema: - "$ref": "#/components/schemas/Error" - security: - - Bearer: [] - ApiKeyAuth: [] "/api/v1/users/{username}/project/{project_name}": get: - summary: Get specific project stats + summary: Get user project details tags: - - Users - description: Get detailed stats for a specific project. + - Stats + description: Returns details for a specific project. parameters: - name: username in: path - description: Username required: true schema: type: string - name: project_name in: path - description: Project Name required: true schema: type: string @@ -3172,26 +3021,22 @@ paths: in: query schema: type: string - format: date_time - description: Stats start time (ISO 8601) + format: date - name: end in: query schema: type: string - format: date_time - description: Stats end time (ISO 8601) + format: date - name: start_date in: query schema: type: string - format: date_time - description: Start date (ISO 8601) for stats calculation + format: date - name: end_date in: query schema: type: string - format: date_time - description: End date (ISO 8601) for stats calculation + format: date responses: '200': description: successful @@ -3221,25 +3066,22 @@ paths: type: string format: date_time nullable: true - security: - - Bearer: [] - ApiKeyAuth: [] "/api/v1/users/{username}/projects/details": get: - summary: Get detailed project stats + summary: Get details for multiple projects tags: - - Users - description: Get detailed breakdown of all user projects. + - Stats + description: Returns details for multiple projects, or all projects in a time + range. parameters: - name: username in: path - description: Username required: true schema: type: string - name: projects in: query - description: Comma-separated list of projects to filter + description: Comma-separated project names schema: type: string - name: since @@ -3247,43 +3089,31 @@ paths: schema: type: string format: date_time - description: Start time (ISO 8601) for project discovery - name: until in: query schema: type: string format: date_time - description: End time (ISO 8601) for project discovery - - name: until_date - in: query - schema: - type: string - format: date_time - description: End time (ISO 8601) for project discovery - name: start in: query schema: type: string format: date_time - description: Stats start time (ISO 8601) - name: end in: query schema: type: string format: date_time - description: Stats end time (ISO 8601) - name: start_date in: query schema: type: string - format: date_time - description: Start date (ISO 8601) for stats calculation + format: date - name: end_date in: query schema: type: string - format: date_time - description: End date (ISO 8601) for stats calculation + format: date responses: '200': description: successful @@ -3318,18 +3148,12 @@ paths: type: string format: date_time nullable: true - security: - - Bearer: [] - ApiKeyAuth: [] "/api/v1/users/{username}/stats": get: summary: Get user stats tags: - Stats description: User has disabled public stats lookup - security: - - Bearer: [] - ApiKeyAuth: [] parameters: - name: username in: path @@ -3425,7 +3249,6 @@ paths: that need to map emails to Hackatime users. Requires STATS_API_KEY. security: - Bearer: [] - ApiKeyAuth: [] parameters: - name: email in: path @@ -3468,7 +3291,6 @@ paths: description: Find a user ID by their Slack User ID. Requires STATS_API_KEY. security: - Bearer: [] - ApiKeyAuth: [] parameters: - name: slack_uid in: path @@ -3503,41 +3325,16 @@ paths: slack_uid: type: string example: U000000 - "/sailors_log/slack/commands": - post: - summary: Handle Sailor's Log Command - tags: - - Slack - description: Handle incoming Slack slash commands for Sailor's Log (/sailorslog). - parameters: [] - responses: - '200': - description: successful - content: - application/json: - schema: - type: object - properties: - response_type: - type: string - text: - type: string - nullable: true - blocks: - type: array - items: - type: object - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: string components: securitySchemes: Bearer: type: http scheme: bearer - description: User API Key from settings, prefixed with "Bearer" + description: Bearer token accepted by the endpoint + BasicApiKey: + type: http + scheme: basic + description: User API key as the Basic auth credential AdminToken: type: http scheme: bearer