diff --git a/backend/Gemfile b/backend/Gemfile index 40b8c5e..7ead0ba 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -20,4 +20,6 @@ gem 'rack', '~> 3.2' gem 'omniauth' gem 'omniauth_openid_connect' +gem 'omniauth-oauth2' gem 'rack-session' +gem 'faraday' diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index fc33ae1..8db602b 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -84,6 +84,8 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects + jwt (3.1.2) + base64 logger (1.7.0) mail (2.9.0) logger @@ -93,6 +95,8 @@ GEM net-smtp mini_mime (1.1.5) minitest (5.26.2) + multi_xml (0.7.2) + bigdecimal (~> 3.1) mustermann (3.0.4) ruby2_keywords (~> 0.0.1) mustermann-grape (1.1.0) @@ -115,11 +119,22 @@ GEM faraday (>= 1.0, < 3.0) faraday-net_http_persistent net-http-persistent + oauth2 (2.0.18) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) omniauth (2.1.4) hashie (>= 3.4.6) logger rack (>= 2.2.3) rack-protection + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) omniauth_openid_connect (0.8.0) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) @@ -158,6 +173,9 @@ GEM rack (>= 3) ruby2_keywords (0.0.5) securerandom (0.4.1) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) swd (2.0.3) activesupport (>= 3) attr_required (>= 0.0.5) @@ -170,6 +188,7 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + version_gem (1.1.9) webfinger (2.1.3) activesupport faraday (~> 2.0) @@ -183,8 +202,10 @@ PLATFORMS DEPENDENCIES airctiverecord (~> 0.2.1) dotenv (~> 3.2) + faraday grape omniauth + omniauth-oauth2 omniauth_openid_connect puma (~> 7.1) rack (~> 3.2) diff --git a/backend/api/auth.rb b/backend/api/auth.rb index b406472..ccecf38 100644 --- a/backend/api/auth.rb +++ b/backend/api/auth.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'faraday' +require 'json' + class Auth < Grape::API format :json helpers SessionHelpers @@ -11,11 +14,8 @@ class Auth < Grape::API get 'oidc/callback' do auth = env['omniauth.auth'] - session[:user] = { - id: auth.uid, - email: auth.info.email, - name: auth.info.name - } + user = User.find_or_create_from_oidc(auth) + session[:user_id] = user.id redirect ENV.fetch('AUTH_SUCCESS_REDIRECT', '/') end @@ -28,5 +28,45 @@ class Auth < Grape::API error!('Unauthorized', 401) unless current_user current_user end + + get 'hackatime/callback' do + error!('Please log in first', 401) unless current_user + + auth = env['omniauth.auth'] + access_token = auth&.credentials&.token.to_s + + uid = nil + if access_token.present? + begin + conn = Faraday.new(url: 'https://hackatime.hackclub.com') + response = conn.get('/api/v1/authenticated/me') do |req| + req.headers['Authorization'] = "Bearer #{access_token}" + req.headers['Accept'] = 'application/json' + end + if response.success? + body = JSON.parse(response.body) rescue {} + uid = body['id'].to_s + end + rescue Faraday::Error => e + error!("Hackatime API error: #{e.message}", 502) + rescue StandardError => e + error!("Unexpected error: #{e.message}", 500) + end + end + + error!('Could not determine Hackatime user. Try again.', 400) if uid.blank? + + session[:hackatime] = { + uid: uid, + access_token: access_token + } + + redirect ENV.fetch('HACKATIME_SUCCESS_REDIRECT', '/') + end + + get :hackatime do + error!('Unauthorized', 401) unless current_user + session[:hackatime] || {} + end end end diff --git a/backend/api/session_helpers.rb b/backend/api/session_helpers.rb index 7022980..24c666e 100644 --- a/backend/api/session_helpers.rb +++ b/backend/api/session_helpers.rb @@ -6,7 +6,7 @@ module SessionHelpers end def current_user - session[:user] + @current_user ||= User.find(session[:user_id]) if session[:user_id] end def authenticate! diff --git a/backend/config.ru b/backend/config.ru index 3878ebd..e0fccc6 100644 --- a/backend/config.ru +++ b/backend/config.ru @@ -8,6 +8,7 @@ require 'grape' require 'rack/session' require 'omniauth' require 'omniauth_openid_connect' +require 'omniauth-oauth2' use Rack::Session::Cookie, key: 'stickers.session', @@ -30,6 +31,17 @@ use OmniAuth::Builder do scheme: 'https' }, scope: %i[openid email profile name address] + + provider :oauth2, + ENV.fetch('HACKATIME_CLIENT_ID'), + ENV.fetch('HACKATIME_CLIENT_SECRET'), + name: :hackatime, + scope: 'read', + client_options: { + site: 'https://hackatime.hackclub.com', + authorize_url: '/oauth/authorize', + token_url: '/oauth/token' + } end run lambda { |env| diff --git a/backend/models/user.rb b/backend/models/user.rb new file mode 100644 index 0000000..09d5096 --- /dev/null +++ b/backend/models/user.rb @@ -0,0 +1,45 @@ +class User < ApplicationRecord + self.table_name = "tblu0BvCreui4oARV" + + field :email, "email" + field :name, "name" + field :hca_id, "hca_id" + field :hackatime_uid, "hackatime_uid" + field :hackatime_access_token, "hackatime_access_token" + + def self.find_or_create_from_oidc(auth) + user = where(hca_id: auth.uid).first + return user if user + + create( + hca_id: auth.uid, + email: auth.info.email, + name: auth.info.name + ) + end + + def link_hackatime!(uid:, access_token:) + self.hackatime_uid = uid + self.hackatime_access_token = access_token + save + end + + def unlink_hackatime! + self.hackatime_uid = nil + self.hackatime_access_token = nil + save + end + + def hackatime_linked? + hackatime_uid.present? + end + + def as_json(options = nil) + { + id: id, + email: email, + name: name, + hackatime_linked: hackatime_linked? + } + end +end