diff --git a/app/controllers/accounts/stack_managers_controller.rb b/app/controllers/accounts/stack_managers_controller.rb index 02b27b05..35bfa84a 100644 --- a/app/controllers/accounts/stack_managers_controller.rb +++ b/app/controllers/accounts/stack_managers_controller.rb @@ -4,23 +4,36 @@ module Accounts before_action :set_stack, only: [ :sync_clusters, :sync_registries ] skip_before_action :authenticate_user!, only: [ :verify_url ] - def authenticated - stack_manager = current_account&.stack_manager - - if stack_manager.nil? || current_user&.portainer_jwt.nil? - head :unauthorized - return + def verify_login + stack_manager = current_account.stack_manager + if stack_manager.nil? + head :not_found end - portainer_client = Portainer::Client.new(stack_manager.provider_url, current_user.portainer_jwt) - - if portainer_client.authenticated? + if stack_manager.stack.client.authenticated? head :ok else head :unauthorized end end + def verify_url + url = params[:url] + begin + response = HTTParty.get(url, timeout: 5, verify: false) + + if response.success? + head :ok + else + head :bad_gateway + end + rescue Net::ReadTimeout, SocketError, Errno::ECONNREFUSED + head :bad_gateway + rescue StandardError + head :internal_server_error + end + end + def index redirect_to stack_manager_path end @@ -90,25 +103,6 @@ module Accounts redirect_to providers_path, notice: "Registries synced successfully" end - def verify_url - url = params[:url] - - begin - response = HTTParty.get(url, timeout: 5, verify: false) - - if response.success? - render json: { success: true } - else - render json: { success: false, message: "Server returned status #{response.code}" } - end - rescue Net::ReadTimeout - render json: { success: false, message: "Connection timeout - server took too long to respond" } - rescue SocketError, Errno::ECONNREFUSED - render json: { success: false, message: "Unable to connect - please check the URL" } - rescue StandardError => e - render json: { success: false, message: "Error: #{e.message}" } - end - end private diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 46763820..43df8e2a 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -19,9 +19,17 @@ class Users::SessionsController < Devise::SessionsController super do # If the account has a stack manager that provides authentication, # redirect to the custom account login URL after logout - if account&.stack_manager&.stack&.provides_authentication? - return redirect_to account_sign_in_path(account.slug), notice: "Signed out successfully." + redirect_url = if account&.stack_manager&.stack&.provides_authentication? + account_sign_in_path(account.slug) + else + root_path end + + respond_to do |format| + format.html { redirect_to redirect_url, notice: "Signed out successfully." } + format.json { render json: { redirect_url: redirect_url }, status: :ok } + end + return end end diff --git a/app/javascript/controllers/stack_manager/badge_controller.js b/app/javascript/controllers/stack_manager/badge_controller.js new file mode 100644 index 00000000..98c6bf6d --- /dev/null +++ b/app/javascript/controllers/stack_manager/badge_controller.js @@ -0,0 +1,57 @@ +import { Controller } from "@hotwired/stimulus" +import { PortainerChecker } from "../../utils/portainer" + +const AUTHENTICATION_VERIFICATION_METHOD = "authentication"; +const URL_VERIFICATION_METHOD = "url"; + +export default class extends Controller { + static targets = [ "message", "verifyUrlSuccess", "verifyUrlError", "verifyUrlLoading" ] + + static values = { + verificationMethod: String, + verifyUrl: String, + } + + async connect() { + this.verifyUrlLoadingTarget.classList.remove('hidden') + const portainerChecker = new PortainerChecker(); + let result = null; + if (this.verificationMethodValue === AUTHENTICATION_VERIFICATION_METHOD) { + result = await portainerChecker.verifyPortainerAuthentication(); + } else if (this.verificationMethodValue === URL_VERIFICATION_METHOD) { + const url = this.verifyUrlValue; + result = await portainerChecker.verifyPortainerUrl(url); + } + + if (result === PortainerChecker.STATUS_UNAUTHORIZED) { + this.logout(); + } else if (result === PortainerChecker.STATUS_OK) { + this.verifyUrlSuccessTarget.classList.remove('hidden') + } else { + this.verifyUrlErrorTarget.classList.remove('hidden') + } + this.verifyUrlLoadingTarget.classList.add('hidden') + } + + async logout() { + try { + const response = await fetch('/users/sign_out', { + method: 'DELETE', + headers: { + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content, + 'Accept': 'application/json' + }, + credentials: 'same-origin' + }) + + if (response.ok) { + const data = await response.json() + window.location.href = data.redirect_url + } else { + window.location.href = '/users/sign_in' + } + } catch (error) { + window.location.href = '/users/sign_in' + } + } +} diff --git a/app/javascript/controllers/stack_manager/url_input_controller.js b/app/javascript/controllers/stack_manager/url_input_controller.js new file mode 100644 index 00000000..dd0025ef --- /dev/null +++ b/app/javascript/controllers/stack_manager/url_input_controller.js @@ -0,0 +1,65 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ + "urlInput", + "verifyUrlSuccess", + "verifyUrlError", + "verifyUrlLoading", + ] + + async verifyUrl(event) { + const url = this.urlInputTarget.value.trim() + + this.hideAllStatuses() + this.showLoading() + + try { + const response = await fetch('/stack_manager/verify_url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ url: url }) + }) + + if (response.status === 401) { + this.showError('Unauthorized') + return + } + + if (response.ok) { + this.showSuccess() + } else { + this.showError('Unable to connect') + } + } catch (error) { + this.showError('Network error - please check the URL') + } + } + + showLoading() { + this.hideAllStatuses() + this.verifyUrlLoadingTarget.classList.remove('hidden') + } + + showSuccess() { + this.hideAllStatuses() + this.verifyUrlSuccessTarget.classList.remove('hidden') + } + + showError(message) { + this.hideAllStatuses() + if (this.hasErrorMessageTarget && message) { + this.errorMessageTarget.textContent = message + } + this.verifyUrlErrorTarget.classList.remove('hidden') + } + + hideAllStatuses() { + this.verifyUrlSuccessTarget.classList.add('hidden') + this.verifyUrlErrorTarget.classList.add('hidden') + this.verifyUrlLoadingTarget.classList.add('hidden') + } +} diff --git a/app/javascript/controllers/stack_manager_url_controller.js b/app/javascript/controllers/stack_manager_url_controller.js deleted file mode 100644 index 61d87c5c..00000000 --- a/app/javascript/controllers/stack_manager_url_controller.js +++ /dev/null @@ -1,100 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = ["urlInput", "verifyUrlSuccess", "verifyUrlError", "verifyUrlLoading", "errorMessage"] - static values = { - verifyUrl: String, - logoutOnFailure: Boolean - } - - connect() { - if (this.urlInputTarget.value) { - this.verifyUrl() - } - } - - async verifyUrl(event) { - const url = this.urlInputTarget.value.trim() - - if (!url) { - this.hideAllStatuses() - return - } - - this.showLoading() - - try { - const response = await fetch(this.verifyUrlValue, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content - }, - body: JSON.stringify({ url: url }) - }) - - const data = await response.json() - - if (data.success) { - this.showSuccess() - } else { - this.showError(data.message || 'Unable to connect to Portainer') - if (this.logoutOnFailureValue) { - await this.logout() - } - } - } catch (error) { - this.showError('Network error - please check the URL') - if (this.logoutOnFailureValue) { - await this.logout() - } - } - } - - showLoading() { - this.hideAllStatuses() - this.verifyUrlLoadingTarget.classList.remove('hidden') - } - - showSuccess() { - this.hideAllStatuses() - this.verifyUrlSuccessTarget.classList.remove('hidden') - } - - showError(message) { - this.hideAllStatuses() - if (this.hasErrorMessageTarget && message) { - this.errorMessageTarget.textContent = message - } - this.verifyUrlErrorTarget.classList.remove('hidden') - } - - hideAllStatuses() { - this.verifyUrlSuccessTarget.classList.add('hidden') - this.verifyUrlErrorTarget.classList.add('hidden') - this.verifyUrlLoadingTarget.classList.add('hidden') - } - - async logout() { - try { - const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content - - const response = await fetch('/users/sign_out', { - method: 'DELETE', - headers: { - 'X-CSRF-Token': csrfToken, - 'Accept': 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml', - 'X-Requested-With': 'XMLHttpRequest' - }, - credentials: 'same-origin' - }) - - if (response.ok) { - window.location.href = '/users/sign_in' - } - } catch (error) { - console.error('Logout failed:', error) - window.location.href = '/users/sign_in' - } - } -} diff --git a/app/javascript/utils/portainer.js b/app/javascript/utils/portainer.js new file mode 100644 index 00000000..7123e0ab --- /dev/null +++ b/app/javascript/utils/portainer.js @@ -0,0 +1,50 @@ +export class PortainerChecker { + static STATUS_OK = "ok"; + static STATUS_UNAUTHORIZED = "unauthorized"; + static STATUS_ERROR = "error"; + + csrfToken() { + return document.querySelector('meta[name="csrf-token"]').content + } + + async verifyPortainerAuthentication() { + const response = await fetch('/stack_manager/verify_login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken() + } + }) + + return this.toResult(response); + } + + toResult(response) { + if (response.status === 401) { + return PortainerChecker.STATUS_UNAUTHORIZED; + } + + if (response.status === 502) { + return PortainerChecker.STATUS_ERROR; + } + + if (response.ok) { + return PortainerChecker.STATUS_OK; + } + + return PortainerChecker.STATUS_ERROR; + } + + async verifyPortainerUrl(url) { + const response = await fetch('/stack_manager/verify_url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken() + }, + body: JSON.stringify({ url: url }) + }) + + return this.toResult(response); + } +} \ No newline at end of file diff --git a/app/views/accounts/stack_managers/_show.html.erb b/app/views/accounts/stack_managers/_show.html.erb index 85a12516..5e6322b7 100644 --- a/app/views/accounts/stack_managers/_show.html.erb +++ b/app/views/accounts/stack_managers/_show.html.erb @@ -28,7 +28,7 @@
<%= stack_manager.stack_manager_type.humanize %>
-
+
Provider URL
@@ -40,10 +40,10 @@ :provider_url, stack_manager.provider_url, class: "hidden input input-bordered w-full", - data: { stack_manager_url_target: "urlInput" } + data: { "stack-manager--url-input-target": "urlInput" } ) %>
- + <%= render "accounts/stack_managers/url_connection_statuses" %>
diff --git a/app/views/accounts/stack_managers/_url.html.erb b/app/views/accounts/stack_managers/_url.html.erb index f7a1d872..d2108c60 100644 --- a/app/views/accounts/stack_managers/_url.html.erb +++ b/app/views/accounts/stack_managers/_url.html.erb @@ -1,11 +1,11 @@ -
+
<%= form.label :provider_url, "Portainer URL" %> <%= form.text_field( :provider_url, name: "stack_manager[provider_url]", class: "input input-bordered w-full", placeholder: "http://portainer.portainer.svc.cluster.local:9000", - data: { stack_manager_url_target: "urlInput", action: "blur->stack-manager-url#verifyUrl" }, + data: { "stack-manager--url-input-target": "urlInput", action: "blur->stack-manager--url-input#verifyUrl" }, ) %>
diff --git a/app/views/accounts/stack_managers/_url_connection_statuses.html.erb b/app/views/accounts/stack_managers/_url_connection_statuses.html.erb index a60beac2..37af57d6 100644 --- a/app/views/accounts/stack_managers/_url_connection_statuses.html.erb +++ b/app/views/accounts/stack_managers/_url_connection_statuses.html.erb @@ -1,12 +1,12 @@ -