diff --git a/app/controllers/admin/admin_api_keys_controller.rb b/app/controllers/admin/admin_api_keys_controller.rb new file mode 100644 index 0000000..991349e --- /dev/null +++ b/app/controllers/admin/admin_api_keys_controller.rb @@ -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 diff --git a/app/controllers/api/admin/application_controller.rb b/app/controllers/api/admin/application_controller.rb new file mode 100644 index 0000000..51917f1 --- /dev/null +++ b/app/controllers/api/admin/application_controller.rb @@ -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 diff --git a/app/controllers/api/admin/v1/application_controller.rb b/app/controllers/api/admin/v1/application_controller.rb new file mode 100644 index 0000000..3faf901 --- /dev/null +++ b/app/controllers/api/admin/v1/application_controller.rb @@ -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 diff --git a/app/controllers/api/admin/v1/stats_controller.rb b/app/controllers/api/admin/v1/stats_controller.rb new file mode 100644 index 0000000..9b81450 --- /dev/null +++ b/app/controllers/api/admin/v1/stats_controller.rb @@ -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 diff --git a/app/controllers/api/admin/v1/users_controller.rb b/app/controllers/api/admin/v1/users_controller.rb new file mode 100644 index 0000000..dcec3ba --- /dev/null +++ b/app/controllers/api/admin/v1/users_controller.rb @@ -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 diff --git a/app/models/admin_api_key.rb b/app/models/admin_api_key.rb new file mode 100644 index 0000000..134b2ab --- /dev/null +++ b/app/models/admin_api_key.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 16cb108..f1445aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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, diff --git a/app/views/admin/admin_api_keys/index.html.erb b/app/views/admin/admin_api_keys/index.html.erb new file mode 100644 index 0000000..b2f2814 --- /dev/null +++ b/app/views/admin/admin_api_keys/index.html.erb @@ -0,0 +1,77 @@ +<% content_for :title, "Admin API Keys" %> + +
fraud team is gonna foam at the mouth for this shit
+| name | +spawned by | +spawned at | +token | +perform | +
|---|---|---|---|---|
|
+
+ <%= api_key.name %>
+
+ |
+
+
+ <% if api_key.user.avatar_url %>
+
+
+
+ <%= api_key.user.display_name %>
+ ID: <%= api_key.user.id %>
+ |
+ + <%= api_key.created_at.strftime("%b %d, %Y at %I:%M %p") %> + | +
+
+ <%= api_key.token[0..12] %>...
+
+ |
+ + <%= 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" %> + | +
you can make a new key if you wanna, or not
+its geting real
+get the deets
++ copy it now, its not gonna be shown again silly +
+
+ <%= flash[:api_key_token] %>
+
+
+ <%= @admin_api_key.token[0..20] %>...
+
+ <% unless flash[:api_key_token] %>
+ + you cant see the thing again, we showed it when you created it ya doofus +
+ <% end %> +
+ Authorization: Bearer YOUR_KEY_HERE
+
+
+ replace YOUR_KEY_HERE with your actual api key, pass it as a header and your balling
+
+ <%= request.protocol %><%= request.host_with_port %>/api/admin/v1/
+
+