mirror of
https://github.com/czhu12/canine.git
synced 2025-12-20 10:19:50 -06:00
fix verify_url
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
57
app/javascript/controllers/stack_manager/badge_controller.js
Normal file
57
app/javascript/controllers/stack_manager/badge_controller.js
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/javascript/utils/portainer.js
Normal file
50
app/javascript/utils/portainer.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="font-medium"><%= stack_manager.stack_manager_type.humanize %></div>
|
||||
</div>
|
||||
|
||||
<div data-controller="stack-manager-url" data-stack-manager-url-verify-url-value="<%= verify_url_stack_manager_path %>">
|
||||
<div data-controller="stack-manager--url-input">
|
||||
<div class="text-sm text-base-content/70">Provider URL</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-medium">
|
||||
@@ -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" }
|
||||
) %>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-action="stack-manager-url#verifyUrl">Test Connection <iconify-icon icon="lucide:refresh-cw"></iconify-icon></button>
|
||||
<button class="btn btn-ghost btn-sm" data-action="stack-manager--url-input#verifyUrl">Test Connection <iconify-icon icon="lucide:refresh-cw"></iconify-icon></button>
|
||||
<%= render "accounts/stack_managers/url_connection_statuses" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div data-controller="stack-manager-url" data-stack-manager-url-verify-url-value="<%= verify_url_stack_manager_path %>">
|
||||
<div data-controller="stack-manager--url-input">
|
||||
<%= 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" },
|
||||
) %>
|
||||
<div class="label justify-end">
|
||||
<span class="label-text-alt">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="text-success flex items-center gap-x-0.5 hidden" data-stack-manager-url-target="verifyUrlSuccess">
|
||||
<div class="text-success flex items-center gap-x-0.5 hidden" data-stack-manager--url-input-target="verifyUrlSuccess">
|
||||
Connection successful
|
||||
<iconify-icon icon="lucide:circle-check"></iconify-icon>
|
||||
</div>
|
||||
<div class="text-error flex items-center gap-x-0.5 hidden" data-stack-manager-url-target="verifyUrlError">
|
||||
<span data-stack-manager-url-target="errorMessage">Connection failed</span>
|
||||
<div class="text-error flex items-center gap-x-0.5 hidden" data-stack-manager--url-input-target="verifyUrlError">
|
||||
<span data-stack-manager--url-input-target="errorMessage">Connection failed</span>
|
||||
<iconify-icon icon="lucide:circle-x"></iconify-icon>
|
||||
</div>
|
||||
<div class="text-gray-500 flex items-center gap-x-0.5 hidden" data-stack-manager-url-target="verifyUrlLoading">
|
||||
<div class="text-gray-500 flex items-center gap-x-0.5 hidden" data-stack-manager--url-input-target="verifyUrlLoading">
|
||||
Loading
|
||||
<iconify-icon class="animate-spin" icon="lucide:loader-circle"></iconify-icon>
|
||||
</div>
|
||||
@@ -1,24 +1,38 @@
|
||||
<%
|
||||
verify_url ||= verify_url_stack_manager_path
|
||||
logout_on_failure ||= false
|
||||
verification_method ||= "url"
|
||||
logout_on_unauthorized ||= false
|
||||
%>
|
||||
<div class="border-t border-base-300/50 transition-all duration-300 group-hover:border-base-300"
|
||||
data-controller="stack-manager-url"
|
||||
data-stack-manager-url-verify-url-value="<%= verify_url %>"
|
||||
data-stack-manager-url-logout-on-failure-value="<%= logout_on_failure %>">
|
||||
<input type="hidden" data-stack-manager-url-target="urlInput" value="<%= stack_manager.provider_url %>" />
|
||||
<div
|
||||
class="border-t border-base-300/50 transition-all duration-300 group-hover:border-base-300"
|
||||
data-controller="stack-manager--badge"
|
||||
data-stack-manager--badge-verification-method-value="<%= verification_method %>"
|
||||
data-stack-manager--badge-verify-url-value="<%= stack_manager.provider_url %>"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<%= link_to stack_manager.provider_url, target: "_blank", rel: "noopener", class: "group/badge inline-flex" do %>
|
||||
<div class="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-blue-500/10 text-blue-400 border border-blue-500/20 transition-all duration-300 hover:bg-blue-500/25 hover:border-blue-500/40 hover:text-blue-300 cursor-pointer">
|
||||
<iconify-icon icon="lucide:server" width="14" height="14"></iconify-icon>
|
||||
<span class="font-medium">Managed via Portainer</span>
|
||||
<div class="tooltip tooltip-bottom flex hidden" data-tip="Portainer is reachable" data-stack-manager-url-target="verifyUrlSuccess">
|
||||
<div
|
||||
class="tooltip tooltip-bottom flex hidden"
|
||||
data-tip="Portainer is reachable"
|
||||
data-stack-manager--badge-target="verifyUrlSuccess"
|
||||
>
|
||||
<iconify-icon icon="lucide:external-link" width="12" height="12" class="opacity-80 group-hover/badge:opacity-100 transition-opacity duration-200"></iconify-icon>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom flex hidden" data-tip="Portainer URL cannot be reached" data-stack-manager-url-target="verifyUrlError">
|
||||
<div
|
||||
class="tooltip tooltip-bottom flex hidden"
|
||||
data-tip="Portainer URL cannot be reached"
|
||||
data-stack-manager--badge-target="verifyUrlError"
|
||||
>
|
||||
<iconify-icon icon="lucide:alert-circle" width="12" height="12" class="text-red-400 opacity-80 group-hover/badge:opacity-100 transition-opacity duration-200"></iconify-icon>
|
||||
</div>
|
||||
<iconify-icon icon="lucide:loader-2" width="12" height="12" class="opacity-80 animate-spin hidden" data-stack-manager-url-target="verifyUrlLoading"></iconify-icon>
|
||||
<iconify-icon
|
||||
icon="lucide:loader-2"
|
||||
width="12"
|
||||
height="12"
|
||||
class="opacity-80 animate-spin"
|
||||
data-stack-manager--badge-target="verifyUrlLoading"></iconify-icon>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render "devise/sessions/portainer_badge",
|
||||
stack_manager: current_account.stack_manager,
|
||||
verify_url: authenticated_stack_manager_path,
|
||||
logout_on_failure: true %>
|
||||
verification_method: "authentication",
|
||||
logout_on_unauthorized: true %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center justify-center">
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<div class="flex flex-row gap-x-4 items-stretch">
|
||||
<div class="w-64 space-y-4">
|
||||
<% if current_account.stack_manager.present? && current_account.stack_manager.stack.provides_authentication? %>
|
||||
<div class="rounded bg-base-200 p-4">
|
||||
<div class="mb-4">
|
||||
<%= render "devise/sessions/portainer_badge", stack_manager: current_account.stack_manager %>
|
||||
</div>
|
||||
<div class="rounded bg-base-200 pl-4 pr-2 py-4">
|
||||
<h3 class="text-sm font-semibold mb-3">Account Login URL</h3>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<input type="text"
|
||||
|
||||
@@ -28,7 +28,7 @@ Rails.application.routes.draw do
|
||||
resource :stack_manager, only: %i[show new create edit update destroy], controller: 'accounts/stack_managers' do
|
||||
collection do
|
||||
post :verify_url
|
||||
get :authenticated
|
||||
post :verify_login
|
||||
post :sync_clusters
|
||||
post :sync_registries
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user