mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
add support for admin api keys
This commit is contained in:
41
app/controllers/admin/admin_api_keys_controller.rb
Normal file
41
app/controllers/admin/admin_api_keys_controller.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class Admin::AdminApiKeysController < Admin::BaseController
|
||||
before_action :set_admin_api_key, only: [ :show, :destroy ]
|
||||
|
||||
def index
|
||||
@admin_api_keys = AdminApiKey.includes(:user).active.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@admin_api_key = current_user.admin_api_keys.build
|
||||
end
|
||||
|
||||
def create
|
||||
@admin_api_key = current_user.admin_api_keys.build(admin_api_key_params)
|
||||
|
||||
if @admin_api_key.save
|
||||
flash[:notice] = "created! now go have fun with it"
|
||||
flash[:api_key_token] = @admin_api_key.token
|
||||
redirect_to admin_admin_api_key_path(@admin_api_key)
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@admin_api_key.revoke!
|
||||
redirect_to admin_admin_api_keys_path, notice: "the key has been revoked"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_admin_api_key
|
||||
@admin_api_key = AdminApiKey.find(params[:id])
|
||||
end
|
||||
|
||||
def admin_api_key_params
|
||||
params.require(:admin_api_key).permit(:name)
|
||||
end
|
||||
end
|
||||
40
app/controllers/api/admin/application_controller.rb
Normal file
40
app/controllers/api/admin/application_controller.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
module Api
|
||||
module Admin
|
||||
class ApplicationController < ActionController::API
|
||||
include ActionController::HttpAuthentication::Token::ControllerMethods
|
||||
|
||||
before_action :authenticate_admin_api_key!
|
||||
|
||||
private
|
||||
|
||||
def authenticate_admin_api_key!
|
||||
authenticate_or_request_with_http_token do |token, options|
|
||||
@admin_api_key = AdminApiKey.active.find_by(token: token)
|
||||
|
||||
if @admin_api_key
|
||||
@current_user = @admin_api_key.user
|
||||
@current_user.admin?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user
|
||||
end
|
||||
|
||||
def current_admin_api_key
|
||||
@admin_api_key
|
||||
end
|
||||
|
||||
def render_unauthorized
|
||||
render json: { error: "lmao no perms" }, status: :unauthorized
|
||||
end
|
||||
|
||||
def render_forbidden
|
||||
render json: { error: "lmao no perms" }, status: :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
9
app/controllers/api/admin/v1/application_controller.rb
Normal file
9
app/controllers/api/admin/v1/application_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
module Api
|
||||
module Admin
|
||||
module V1
|
||||
class ApplicationController < Api::Admin::ApplicationController
|
||||
# todo, add shit here or something
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
74
app/controllers/api/admin/v1/stats_controller.rb
Normal file
74
app/controllers/api/admin/v1/stats_controller.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
module Api
|
||||
module Admin
|
||||
module V1
|
||||
class StatsController < Api::Admin::V1::ApplicationController
|
||||
def index
|
||||
render json: {
|
||||
platform_stats: {
|
||||
total_users: User.count,
|
||||
admin_users: User.where(admin: true).count,
|
||||
active_users_last_7_days: User.joins(:heartbeats)
|
||||
.where(heartbeats: { time: 7.days.ago.. })
|
||||
.distinct.count,
|
||||
active_users_last_30_days: User.joins(:heartbeats)
|
||||
.where(heartbeats: { time: 30.days.ago.. })
|
||||
.distinct.count,
|
||||
total_heartbeats: Heartbeat.count,
|
||||
heartbeats_last_7_days: Heartbeat.where(time: 7.days.ago..).count,
|
||||
heartbeats_last_30_days: Heartbeat.where(time: 30.days.ago..).count,
|
||||
total_api_keys: ApiKey.count,
|
||||
total_admin_api_keys: AdminApiKey.active.count,
|
||||
trust_levels: User.group(:trust_level).count
|
||||
},
|
||||
generated_at: Time.current
|
||||
}
|
||||
end
|
||||
|
||||
def heartbeats
|
||||
limit = [ params[:limit]&.to_i || 100, 1000 ].min
|
||||
offset = params[:offset]&.to_i || 0
|
||||
|
||||
heartbeats = Heartbeat.includes(:user)
|
||||
.order(time: :desc)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
if params[:user_id].present?
|
||||
heartbeats = heartbeats.where(user_id: params[:user_id])
|
||||
end
|
||||
|
||||
if params[:from].present?
|
||||
heartbeats = heartbeats.where("time >= ?", Time.parse(params[:from]))
|
||||
end
|
||||
|
||||
if params[:to].present?
|
||||
heartbeats = heartbeats.where("time <= ?", Time.parse(params[:to]))
|
||||
end
|
||||
|
||||
render json: {
|
||||
heartbeats: heartbeats.map do |hb|
|
||||
{
|
||||
id: hb.id,
|
||||
user_id: hb.user_id,
|
||||
user_display_name: hb.user&.display_name,
|
||||
time: hb.time,
|
||||
project: hb.project,
|
||||
language: hb.language,
|
||||
entity: hb.entity,
|
||||
duration: hb.duration,
|
||||
is_debugging: hb.is_debugging
|
||||
}
|
||||
end,
|
||||
meta: {
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
total_count: heartbeats.count
|
||||
}
|
||||
}
|
||||
rescue ArgumentError => e
|
||||
render json: { error: "tf is that date buddy #{e.message}" }, status: :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
120
app/controllers/api/admin/v1/users_controller.rb
Normal file
120
app/controllers/api/admin/v1/users_controller.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
module Api
|
||||
module Admin
|
||||
module V1
|
||||
class UsersController < Api::Admin::V1::ApplicationController
|
||||
def index
|
||||
users = User.includes(:email_addresses, :api_keys)
|
||||
.order(created_at: :desc)
|
||||
.limit(params[:limit]&.to_i || 50)
|
||||
|
||||
if params[:admin_only] == "true"
|
||||
users = users.where(admin: true)
|
||||
end
|
||||
|
||||
render json: {
|
||||
users: users.map do |user|
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
slack_uid: user.slack_uid,
|
||||
github_username: user.github_username,
|
||||
admin: user.admin?,
|
||||
superadmin: user.superadmin?,
|
||||
trust_level: user.trust_level,
|
||||
timezone: user.timezone,
|
||||
created_at: user.created_at,
|
||||
last_heartbeat_at: user.heartbeats.maximum(:time),
|
||||
api_keys_count: user.api_keys.count,
|
||||
total_heartbeats: user.heartbeats.count
|
||||
}
|
||||
end,
|
||||
meta: {
|
||||
total_count: users.count,
|
||||
limit: params[:limit]&.to_i || 50
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show
|
||||
user = User.find(params[:id])
|
||||
|
||||
render json: {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
slack_uid: user.slack_uid,
|
||||
slack_username: user.slack_username,
|
||||
github_username: user.github_username,
|
||||
admin: user.admin?,
|
||||
superadmin: user.superadmin?,
|
||||
trust_level: user.trust_level,
|
||||
timezone: user.timezone,
|
||||
country_code: user.country_code,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
last_heartbeat_at: user.heartbeats.maximum(:time),
|
||||
api_keys: user.api_keys.map do |key|
|
||||
{
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
token_preview: "#{key.token[0..8]}...",
|
||||
created_at: key.created_at
|
||||
}
|
||||
end,
|
||||
email_addresses: user.email_addresses.map(&:email),
|
||||
stats: {
|
||||
total_heartbeats: user.heartbeats.count,
|
||||
total_coding_time: user.heartbeats.sum(:duration) || 0,
|
||||
languages_used: user.heartbeats.distinct.pluck(:language).compact.count,
|
||||
projects_worked_on: user.heartbeats.distinct.pluck(:project).compact.count,
|
||||
days_active: user.heartbeats.distinct.pluck(:date).compact.count
|
||||
}
|
||||
}
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "who tf is that lmao" }, status: :not_found
|
||||
end
|
||||
|
||||
def update_trust_level
|
||||
user = User.find(params[:id])
|
||||
trust_level = params[:trust_level]
|
||||
reason = params[:reason] || "updated via the api"
|
||||
notes = params[:notes]
|
||||
|
||||
unless User.trust_levels.key?(trust_level)
|
||||
return render json: { error: "tf is that lmao" }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if trust_level == "red" && !current_user.can_convict_users?
|
||||
return render json: { error: "you dont got perms for that lmao" }, status: :forbidden
|
||||
end
|
||||
|
||||
success = user.set_trust(
|
||||
trust_level,
|
||||
changed_by_user: current_user,
|
||||
reason: reason,
|
||||
notes: notes
|
||||
)
|
||||
|
||||
if success
|
||||
render json: {
|
||||
success: true,
|
||||
message: "okay, gotcha, that user is now #{trust_level}",
|
||||
user: {
|
||||
id: user.id,
|
||||
trust_level: user.trust_level,
|
||||
updated_at: user.updated_at
|
||||
}
|
||||
}
|
||||
else
|
||||
render json: { error: "whomp that didnt work" }, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "who tf is that lmao" }, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/models/admin_api_key.rb
Normal file
25
app/models/admin_api_key.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class AdminApiKey < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates :name, presence: true, uniqueness: { scope: :user_id }
|
||||
|
||||
before_validation :generate_token!, on: :create
|
||||
|
||||
scope :active, -> { where(revoked_at: nil) }
|
||||
|
||||
def active?
|
||||
revoked_at.nil?
|
||||
end
|
||||
|
||||
def revoke!
|
||||
update!(revoked_at: Time.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_token!
|
||||
# should be jazzy enough
|
||||
self.token ||= "hka_#{SecureRandom.hex(32)}"
|
||||
end
|
||||
end
|
||||
@@ -73,6 +73,7 @@ class User < ApplicationRecord
|
||||
class_name: "Hackatime::ProjectLabel"
|
||||
|
||||
has_many :api_keys
|
||||
has_many :admin_api_keys, dependent: :destroy
|
||||
|
||||
has_one :sailors_log,
|
||||
foreign_key: :slack_uid,
|
||||
|
||||
77
app/views/admin/admin_api_keys/index.html.erb
Normal file
77
app/views/admin/admin_api_keys/index.html.erb
Normal file
@@ -0,0 +1,77 @@
|
||||
<% content_for :title, "Admin API Keys" %>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">admin api keys</h1>
|
||||
<p class="text-gray-400">fraud team is gonna foam at the mouth for this shit</p>
|
||||
</div>
|
||||
<%= link_to "spawn in a new key",
|
||||
new_admin_admin_api_key_path,
|
||||
class: "bg-primary hover:bg-red text-white px-4 py-2 rounded-lg font-medium transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark rounded-lg overflow-hidden shadow-xl">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="bg-darkless">
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">name</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">spawned by</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">spawned at</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">token</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">perform</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-950">
|
||||
<% @admin_api_keys.each do |api_key| %>
|
||||
<tr class="hover:bg-darkless">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-white">
|
||||
<%= api_key.name %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<% if api_key.user.avatar_url %>
|
||||
<img class="h-6 w-6 rounded-full mr-2" src="<%= api_key.user.avatar_url %>" alt="">
|
||||
<% end %>
|
||||
<div>
|
||||
<div class="text-sm text-white"><%= api_key.user.display_name %></div>
|
||||
<div class="text-xs text-gray-400">ID: <%= api_key.user.id %></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||||
<%= api_key.created_at.strftime("%b %d, %Y at %I:%M %p") %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="text-xs text-white">
|
||||
<%= api_key.token[0..12] %>...
|
||||
</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
||||
<%= link_to "inspect",
|
||||
admin_admin_api_key_path(api_key),
|
||||
class: "text-blue-400 hover:text-blue-300" %>
|
||||
<%= link_to "nuke",
|
||||
admin_admin_api_key_path(api_key),
|
||||
data: { "turbo-method": :delete },
|
||||
class: "text-red-400 hover:text-red-300" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% if @admin_api_keys.empty? %>
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-400 text-lg mb-2">nothing here cuzo</div>
|
||||
<p class="text-gray-500 mb-4">you can make a new key if you wanna, or not</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
49
app/views/admin/admin_api_keys/new.html.erb
Normal file
49
app/views/admin/admin_api_keys/new.html.erb
Normal file
@@ -0,0 +1,49 @@
|
||||
<% content_for :title, "spawn in a new key" %>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">spawn in a new key</h1>
|
||||
<p class="text-gray-400">its geting real</p>
|
||||
</div>
|
||||
<%= link_to "← get me outta here",
|
||||
admin_admin_api_keys_path,
|
||||
class: "text-gray-400 hover:text-white" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark rounded-lg p-8">
|
||||
<%= form_with model: [:admin, @admin_api_key], local: true, class: "space-y-6" do |f| %>
|
||||
<% if @admin_api_key.errors.any? %>
|
||||
<div class="bg-red-900/50 border border-red-500 rounded-lg p-4">
|
||||
<h3 class="text-red-300 font-medium mb-2">you done did it, fix this or else</h3>
|
||||
<ul class="text-red-200 text-sm space-y-1">
|
||||
<% @admin_api_key.errors.full_messages.each do |message| %>
|
||||
<li>• <%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= f.label :name, class: "block text-md font-medium text-white mb-2" %>
|
||||
<%= f.text_field :name,
|
||||
placeholder: "put down something good please",
|
||||
class: "w-full px-3 py-2 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-900/30 border border-yellow-500/50 rounded-lg p-4">
|
||||
<h3 class="text-yellow-300 font-medium mb-2">keep in mind this gives admin access, so dont go sharing it with your dog ya goober. we also track whatever you do, so dont go out banning people or heidi will pipebomb your sorry butt</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<%= link_to "fuck no imma head out",
|
||||
admin_admin_api_keys_path,
|
||||
class: "px-4 py-2 text-gray-400 hover:text-white" %>
|
||||
<%= f.submit "okay lets do this thing",
|
||||
class: "px-6 py-2 bg-primary hover:bg-red text-white rounded-lg font-medium transition-colors" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
137
app/views/admin/admin_api_keys/show.html.erb
Normal file
137
app/views/admin/admin_api_keys/show.html.erb
Normal file
@@ -0,0 +1,137 @@
|
||||
<% content_for :title, "Admin API Key Details" %>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">lookin at <%= @admin_api_key.name %></h1>
|
||||
<p class="text-gray-400">get the deets</p>
|
||||
</div>
|
||||
<%= link_to "← Back to API Keys",
|
||||
admin_admin_api_keys_path,
|
||||
class: "text-gray-400 hover:text-white" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div class="bg-dark rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">deets</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">name</label>
|
||||
<div class="text-white"><%= @admin_api_key.name %></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">spawned by</label>
|
||||
<div class="flex items-center">
|
||||
<% if @admin_api_key.user.avatar_url %>
|
||||
<img class="h-6 w-6 rounded-full mr-2" src="<%= @admin_api_key.user.avatar_url %>" alt="">
|
||||
<% end %>
|
||||
<div>
|
||||
<div class="text-white"><%= @admin_api_key.user.display_name %></div>
|
||||
<div class="text-xs text-gray-400">ID: <%= @admin_api_key.user.id %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">spawned at</label>
|
||||
<div class="text-white"><%= @admin_api_key.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">status</label>
|
||||
<div class="text-green-400">live</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-600">
|
||||
<%= link_to "nuke it",
|
||||
admin_admin_api_key_path(@admin_api_key),
|
||||
data: { "turbo-method": :delete },
|
||||
class: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">how 2 api</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% if flash[:api_key_token] %>
|
||||
<div class="bg-green-900/30 border border-green-500/50 rounded-lg p-4">
|
||||
<h3 class="text-green-300 font-medium mb-2">heres your key!</h3>
|
||||
<p class="text-green-200 text-sm mb-3">
|
||||
copy it now, its not gonna be shown again silly
|
||||
</p>
|
||||
<div class="bg-gray-800 rounded p-3 mb-3">
|
||||
<code class="block text-white text-sm break-all select-all">
|
||||
<%= flash[:api_key_token] %>
|
||||
</code>
|
||||
</div>
|
||||
<button onclick="copy('<%= flash[:api_key_token] %>')"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm">
|
||||
copy
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">preview</label>
|
||||
<code class="block bg-gray-800 px-3 py-2 rounded text-white text-sm">
|
||||
<%= @admin_api_key.token[0..20] %>...
|
||||
</code>
|
||||
<% unless flash[:api_key_token] %>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
you cant see the thing again, we showed it when you created it ya doofus
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">auth stuff</label>
|
||||
<div class="bg-gray-800 rounded p-3">
|
||||
<code class="text-white text-sm">
|
||||
Authorization: Bearer YOUR_KEY_HERE
|
||||
</code>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
replace <code>YOUR_KEY_HERE</code> with your actual api key, pass it as a header and your balling
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">domain</label>
|
||||
<div class="bg-gray-800 rounded p-3">
|
||||
<code class="text-blue-400 text-sm">
|
||||
<%= request.protocol %><%= request.host_with_port %>/api/admin/v1/
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copy(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show temporary success message
|
||||
const button = event.target;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Copied!';
|
||||
button.classList.add('bg-green-500');
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('bg-green-500');
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
console.error('Could not copy text: ', err);
|
||||
alert('Failed to copy to clipboard. Please copy manually.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -103,6 +103,11 @@
|
||||
OAuth2 apps
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% admin_tool(nil, "div") do %>
|
||||
<%= link_to admin_admin_api_keys_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_admin_api_keys_path) || request.path.start_with?('/admin/admin_api_keys') ? 'bg-primary/50 text-primary' : 'hover:bg-[#23272a]'}", data: { action: "click->nav#clickLink" } do %>
|
||||
Admin API Keys
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% admin_tool(nil, "div") do %>
|
||||
<%= link_to flipper_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(flipper_path) ? 'bg-primary/50 text-primary' : 'hover:bg-[#23272a]'}", data: { action: "click->nav#clickLink" } do %>
|
||||
Feature Flags
|
||||
|
||||
@@ -31,6 +31,7 @@ Rails.application.routes.draw do
|
||||
get "ysws_reviews/:record_id", to: "ysws_reviews#show", as: :ysws_review
|
||||
|
||||
resources :trust_level_audit_logs, only: [ :index, :show ]
|
||||
resources :admin_api_keys, except: [ :edit, :update ]
|
||||
end
|
||||
|
||||
if Rails.env.development?
|
||||
@@ -143,6 +144,20 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
# Admin-only API namespace
|
||||
namespace :admin do
|
||||
namespace :v1 do
|
||||
get "stats", to: "stats#index"
|
||||
get "heartbeats", to: "stats#heartbeats"
|
||||
|
||||
resources :users, only: [ :index, :show ] do
|
||||
member do
|
||||
patch :update_trust_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# wakatime compatible summary
|
||||
get "summary", to: "summary#index"
|
||||
|
||||
|
||||
16
db/migrate/20250701142553_create_admin_api_keys.rb
Normal file
16
db/migrate/20250701142553_create_admin_api_keys.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class CreateAdminApiKeys < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :admin_api_keys do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.text :name, null: false
|
||||
t.text :token, null: false
|
||||
t.datetime :revoked_at
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :admin_api_keys, :token, unique: true
|
||||
add_index :admin_api_keys, [ :user_id, :name ], unique: true
|
||||
end
|
||||
end
|
||||
w
|
||||
18
db/schema.rb
generated
18
db/schema.rb
generated
@@ -10,13 +10,25 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_30_000002) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_01_142553) do
|
||||
create_schema "pganalyze"
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
enable_extension "pg_stat_statements"
|
||||
|
||||
create_table "admin_api_keys", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.text "name", null: false
|
||||
t.text "token", null: false
|
||||
t.datetime "revoked_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["token"], name: "index_admin_api_keys_on_token", unique: true
|
||||
t.index ["user_id", "name"], name: "index_admin_api_keys_on_user_id_and_name", unique: true
|
||||
t.index ["user_id"], name: "index_admin_api_keys_on_user_id"
|
||||
end
|
||||
|
||||
create_table "ahoy_events", force: :cascade do |t|
|
||||
t.bigint "visit_id"
|
||||
t.bigint "user_id"
|
||||
@@ -272,6 +284,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_30_000002) do
|
||||
t.datetime "finished_generating_at"
|
||||
t.datetime "deleted_at"
|
||||
t.integer "period_type", default: 0, null: false
|
||||
t.integer "timezone_utc_offset"
|
||||
t.integer "timezone_offset"
|
||||
t.index ["start_date", "period_type", "timezone_offset"], name: "index_leaderboards_on_start_date_period_type_timezone_offset", where: "(deleted_at IS NULL)"
|
||||
t.index ["start_date"], name: "index_leaderboards_on_start_date", where: "(deleted_at IS NULL)"
|
||||
end
|
||||
|
||||
@@ -551,6 +566,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_30_000002) do
|
||||
t.index ["user_id"], name: "index_wakatime_mirrors_on_user_id"
|
||||
end
|
||||
|
||||
add_foreign_key "admin_api_keys", "users"
|
||||
add_foreign_key "api_keys", "users"
|
||||
add_foreign_key "commits", "repositories"
|
||||
add_foreign_key "commits", "users"
|
||||
|
||||
Reference in New Issue
Block a user