Skip to content

Commit

Permalink
migrate dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
jlbyrne committed Apr 24, 2024
1 parent afbd4e7 commit 8cb9745
Show file tree
Hide file tree
Showing 48 changed files with 3,208 additions and 12,179 deletions.
27 changes: 27 additions & 0 deletions app/controllers/api/nextv1/channels_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Api::Nextv1::ChannelsController < Api::Nextv1::BaseController
include ChannelsHelper

before_action :authenticate_publisher!

before_action :setup_current_channel

attr_reader :current_channel

def destroy
success = DeletePublisherChannelJob.perform_now(current_channel.id)

if success
head :no_content
else
render(json: {errors: current_channel.errors}, status: 400)
end
end

private

def setup_current_channel
@current_channel = current_publisher.channels.find(params[:id])
rescue ActiveRecord::RecordNotFound
render(json: {}, status: 404)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "uri"
require "net/http"
require "json"
require "digest"
require "base64"

class Api::Nextv1::Connection::BitflyerConnectionsController < Api::Nextv1::Oauth2Controller
def destroy
I18n.locale = :ja
bitflyer_connection = current_publisher.bitflyer_connection

# Destroy our database records
if bitflyer_connection.destroy
render(json: {}, status: 200)
else
render(json: {errors: I18n.t(
"publishers.gemini_connections.destroy.error",
errors: bitflyer_connection.errors.full_messages.join(", ")
)}, status: 417)
end
end

private

# 1.) Set required state for Oauth2 Implementation
# @debug is an optional flag that will return a json response from the callback
# Helpful for explicit debugging and introspection of access token request response values.
def set_controller_state
@klass = BitflyerConnection
@access_token_response = Oauth2::Responses::BitflyerAccessTokenResponse
end

# 2.) Bitflyer uses code exchange verification: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
def code_challenge
Digest::SHA256.base64digest(code_verifier).chomp("=").tr("+", "-").tr("/", "_")
end

# One way encoded(Varies through time + unique to provider + random/varies through sesion)
def code_verifier
Digest::SHA256.base64digest(current_publisher.current_sign_in_at.to_s + current_publisher.id + current_publisher.session_salt.to_s)
end

# 3.) Generate auth_url using code_challange verification
def authorization_url
@_authorization_url ||= client.authorization_code_url(
state: @state,
scope: @klass.oauth2_config.scope,
code_challenge: code_challenge,
code_challenge_method: "S256"
)
end

# 4.) Make request using code_verifier
def access_token_request
client.access_token(params.require(:code), code_verifier: code_verifier)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Api::Nextv1::Connection::GeminiConnectionsController < Api::Nextv1::Oauth2Controller
def destroy
gemini_connection = current_publisher.gemini_connection

# Destroy our database records
if gemini_connection.destroy
render(json: {}, status: 200)
else
render(json: {errors: I18n.t(
"publishers.gemini_connections.destroy.error",
errors: gemini_connection.errors.full_messages.join(", ")
)}, status: 417)
end
end

private

# 1.) Set required state for Oauth2 Implementation
# @debug is an optional flag that will return a json response from the callback
# Helpful for explicit debugging and introspection of access token request response values.
def set_controller_state
@klass = GeminiConnection
# @debug = true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# NOTE: To test this locally you have to access the app from 127.0.0.1: Uphold does not allow localhost has a valid domain
# and when you redirect back to 127.0.0.1 all your relevant cookies are lost.
#
# When you login through the email url, you need to copy the url and replace localhost with 127.0.0.1
# and create your session with that domain.
#
# Took me a while to figure that out.

class Api::Nextv1::Connection::UpholdConnectionsController < Api::Nextv1::Oauth2Controller
include PublishersHelper

def show
publisher = current_publisher
render(json: {
uphold_status: publisher.uphold_connection&.uphold_status.to_s,
uphold_is_member: publisher.uphold_connection&.is_member? || false,
uphold_status_summary: uphold_status_summary(publisher),
uphold_status_description: uphold_status_description(publisher),
default_currency: publisher.uphold_connection&.default_currency,
uphold_username: publisher.uphold_connection&.uphold_details&.username
}, status: 200)
end

# TODO: Do we update connections???
# def update
# uphold_connection = current_publisher.uphold_connection
# return if uphold_connection.blank?

# send_emails = DateTime.now

# case params[:send_emails]
# when "forever"
# send_emails = UpholdConnection::FOREVER_DATE
# when "next_year"
# send_emails = 1.year.from_now
# end

# uphold_connection.update(send_emails: send_emails)
# end

# publishers/disconnect_uphold
def destroy
# You can't remove your connection if you've been banned/suspended.
# This is how we prevent you from reusing the connection.
if !current_publisher.authorized_to_act? #
head :unauthorized and return
end

current_publisher&.uphold_connection&.destroy
head :ok
end

private

# 1.) Set required state for Oauth2 Implementation
# @debug is an optional flag that will return a json response from the callback
# Helpful for explicit debugging and introspection of access token request response values.
def set_controller_state
@klass = UpholdConnection
@klass.strict_create = true # toggle various restrictions on creating wallets. Useful for debugging.
end
end
119 changes: 119 additions & 0 deletions app/controllers/api/nextv1/crypto_address_for_channels_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require "eth"
require "rbnacl"
require "base58"

class Api::Nextv1::CryptoAddressForChannelsController < Api::Nextv1::BaseController
include PublishersHelper
include Eth

def index
current_channel = current_publisher.channels.find(params[:channel_id])
@crypto_addresses_for_channel = CryptoAddressForChannel.where(channel: current_channel)
render(json: @crypto_addresses_for_channel)
end

def generate_nonce
nonce = SecureRandom.uuid
Rails.cache.write(nonce, current_publisher.id)
render json: {nonce: nonce}
end

def create
signature = params[:transaction_signature]
account_address = params[:account_address]
chain = params[:chain]
message = params[:message]
current_channel = current_publisher.channels.find(params[:channel_id])
@errors = []

# check that the message is a valid nonce and delete after use
valid_message = if Rails.cache.read(message) == current_publisher.id
!!Rails.cache.delete(message)
else
@errors << "message is invalid"
false
end

# check to make sure the user owns the address
verified = valid_message &&
case chain
when "SOL"
verify_solana_address(signature, account_address, message)
when "ETH"
verify_ethereum_address(signature, account_address, message)
else
@errors << "address could not be verified"
false
end

# Create new crypto address, and remove any other addresses on the same chain for the channel
success = verified && replace_crypto_address_for_channel(account_address, chain, current_channel)

if success
render(json: {crypto_address_for_channel: success}, status: 201)
else
render(json: {errors: @errors}, status: 400)
end
end

def change_address
account_address = params[:address]
chain = params[:chain]
current_channel = current_publisher.channels.find(params[:channel_id])

success = replace_crypto_address_for_channel(account_address, chain, current_channel)

if success
render(json: {crypto_address_for_channel: success}, status: 200)
else
render(json: {errors: "address could not be updated"}, status: 400)
end
end

def destroy
chain = params[:chain]
current_channel = current_publisher.channels.find(params[:channel_id])

begin
CryptoAddressForChannel.where(chain: chain, channel: current_channel).first&.destroy!
render(json: {crypto_address_for_channel: true}, status: 200)
rescue => e
LogException.perform(e, publisher: current_publisher)
render(json: {errors: "address could not be deleted"}, status: 400)
end
end

def verify_solana_address(signature, address, message)
verify_key = RbNaCl::VerifyKey.new(Base58.base58_to_binary(address, :bitcoin))
verify_key.verify(Base58.base58_to_binary(signature, :bitcoin), message)
rescue => e
LogException.perform(e, publisher: current_publisher)
false
end

def verify_ethereum_address(signature, address, message)
signature_pubkey = Eth::Signature.personal_recover message, signature
signature_address = Eth::Util.public_key_to_address signature_pubkey
# Eth addresses are case insensitive
signature_address.address.downcase == address.downcase
rescue => e
LogException.perform(e, publisher: current_publisher)
false
end

def replace_crypto_address_for_channel(account_address, chain, channel)
ActiveRecord::Base.transaction do
crypto_address = CryptoAddress.where(publisher: current_publisher, address: account_address, chain: chain, verified: true).first_or_create!
existing_address = CryptoAddressForChannel.where(chain: chain, channel: channel)

if existing_address.length > 0
existing_address.first.destroy!
end

CryptoAddressForChannel.create!(chain: chain, crypto_address: crypto_address, channel: channel)
end
rescue => e
LogException.perform(e, publisher: current_publisher)
false
end
end
24 changes: 24 additions & 0 deletions app/controllers/api/nextv1/crypto_addresses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Api::Nextv1::CryptoAddressesController < Api::Nextv1::BaseController
include PublishersHelper

def index
@crypto_addresses = CryptoAddress.where(publisher_id: current_publisher.id)
render(json: @crypto_addresses)
end

def destroy
begin
@crypto_address = current_publisher.crypto_addresses.find(params[:id])
rescue ActiveRecord::RecordNotFound
return render json: {}, status: 404
end

begin
@crypto_address.destroy!
render(json: {crypto_address: true}, status: 200)
rescue => e
LogException.perform(e, publisher: current_publisher)
render(json: {errors: "address could not be deleted"}, status: 400)
end
end
end
79 changes: 79 additions & 0 deletions app/controllers/api/nextv1/home_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
class Api::Nextv1::HomeController < Api::Nextv1::BaseController
include PublishersHelper

def dashboard
publisher = current_publisher.as_json(only: [:id], methods: [:brave_payable?])
channels = current_publisher.channels.visible.as_json(only: [:details_type, :id, :verified, :verification_status, :verification_details],
methods: [:failed_verification_details, :failed_verification_call_to_action],
include: {
details: {only: [], methods: [:publication_title]}
})
wallet = PublisherWalletGetter.new(
publisher: current_publisher,
include_transactions: true
).perform
regions = Rewards::Parameters.new.fetch_allowed_regions

wallet_data = {
wallet: wallet,
uphold_connection: uphold_wallet,
gemini_connection: gemini_wallet,
bitflyer_connection: bitflyer_wallet,
allowed_regions: regions,
next_deposit_date: next_deposit_date
}

response_data = {
publisher: publisher,
channels: channels,
wallet_data: wallet_data
}

render(json: response_data.to_json, status: 200)
end

# TODO: figure out if we need the 'latest' endpoint
# Public: Requests the Publisher's latest transactions from Eyeshade
#
# Returns the latest settled payment in JSON
def latest
wallet = PublisherWalletGetter.new(
publisher: current_publisher,
include_transactions: true
).perform

render json: {lastSettlement: wallet.last_settlement_balance}
end

private

# Internal: Renders properties associated with an Uphold Wallet Connection
#
# Returns a hash
def uphold_wallet
current_publisher.uphold_connection.as_json(
only: [:default_currency, :uphold_id, :is_member, :oauth_refresh_failed, :payout_failed],
methods: [:can_create_uphold_cards?, :username, :uphold_status, :verify_url]
)
end

# Internal: Renders properties associated with the Gemini Wallet Connection
#
# Returns a hash
def gemini_wallet
current_publisher.gemini_connection.as_json(
only: [:default_currency, :display_name, :recipient_id, :oauth_refresh_failed, :recipient_id_status, :payout_failed],
methods: [:payable?, :verify_url, :valid_country?]
)
end

# Internal: Renders properties associated with the Bitflyer Wallet Connection
#
# Returns a hash
def bitflyer_wallet
current_publisher.bitflyer_connection.as_json(
only: [:default_currency, :display_name, :recipient_id, :oauth_refresh_failed, :payout_failed],
methods: [:payable?, :verify_url]
)
end
end
Loading

0 comments on commit 8cb9745

Please sign in to comment.