Két dolog volt beépítve az engine-be, ami nem az övé. A **képtár** eddig `WarpEngine::Image` volt, pedig a modell teljesen általános: a teletypegames-ben a tagok arcképét is ez hordozza, nem csak a katalógus borítóit. Az `Image` modell, a feltöltött fájlok, az `/api/image/:id` végpont és az admin oldal ezért átkerült a hosztba, az engine pedig adapteren szól hozzá (`WarpEngine::Images`): `url_for` adja a katalógus JSON `imageUrl`-jét, `select_options` a software-form képválasztóját, `build_from_upload` a "tölts fel új képet" ágat. Az alapértelmezés az `Image` osztály, tehát a default útvonal bitre a régi. A `SoftwareImage` (a katalógus-kapcsolat) maradt az engine-ben, és **az `images` tábla nem mozdult**: az engine csak abbahagyta a létrehozását, a generátor írja meg hoszt-kódként. A **CI** eddig végig Woodpecker volt: kliens, aláírás-ellenőrzés, pipeline-receptek, repo-szinkron, secret-kiosztás. Mindez egy adapter mögé került (`WarpEngine.ci`), a Woodpecker-implementáció pedig az engine-ben maradt `WarpEngine::CI::Woodpecker` néven — kliens, adapter, httpsig-ellenőrző és a platformonkénti pipeline-receptek, mert a YAML-dialektus a szolgáltatóé. Az engine saját kódja már nem nevez szolgáltatót: `CI::Repo` és `CI::Run` értékeket kap, `CI::ConnectionError`/`ApiError`/`NotConfigured` hibákat dob, a `Pipeline` pedig `remote_repo_id`-t ad a történelmi `woodpecker_repo_id` kolumna fölött (a tábla itt sem mozdult). `c.ci_adapter = :none` azt jelenti, hogy ez a hoszt nem buildel: az `/api/ci/*` 503, a `/build/config` elutasít, az admin akciók elbújnak. Mindkét seam a hoszt initializerében van kimondva, nem alapértelmezésre hagyva — a hoszt megnevezi, mi a képtára és mi a CI-ja. Törés a 0.6-hoz képest: `image_container_path`, `image_owners`, `ci_platforms`, `ci_update_server`, `ci_extension_public_key(_url)`, `woodpecker_url`, `woodpecker_api_token`, `woodpecker_repo_owner` és a `WarpEngine.woodpecker_configured?` megszűnt; a helyük `c.image_class_name` / `c.image_adapter` és `c.ci_adapter`. Az `/api/ci/*` `latest_run`/`trigger` válasza a normalizált `CI::Run` alakot adja (number, status, branch, message, createdAt, url), a `pipelines` lista pedig `repo_id`-t is közöl a megtartott `woodpecker_repo_id` mellett. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
5.0 KiB
Ruby
135 lines
5.0 KiB
Ruby
ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
|
actions :index, :show, :new, :create, :edit, :update, :destroy
|
|
permit_params :name, :owner_id, :expires_at, :scopes_string, :unrestricted
|
|
|
|
menu parent: "🌀 WarpEngine", priority: 9, label: "🎟️ App Tokens"
|
|
|
|
config.sort_order = "created_at_desc"
|
|
config.batch_actions = false
|
|
|
|
scope :all, default: true
|
|
scope("Active") { |scope| scope.where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
scope("Expired") { |scope| scope.where("expires_at <= ?", Time.current) }
|
|
|
|
CATALOG_SCOPE_SQL = %(JSON_CONTAINS(COALESCE(scopes, '[]'), '"catalog"')).freeze
|
|
scope("Publishing") { |scope| scope.where("NOT #{CATALOG_SCOPE_SQL}") }
|
|
scope("Clients") { |scope| scope.where(CATALOG_SCOPE_SQL) }
|
|
|
|
index do
|
|
id_column
|
|
column :name
|
|
column("Token") { |t| code "#{t.token_prefix}…", style: "font-family:monospace;" }
|
|
column("Owner") { |t| t.owner.try(:email) || t.owner.try(:name) || "#{t.owner_type} ##{t.owner_id}" }
|
|
column("Scopes") { |t| t.scopes_string }
|
|
column :unrestricted
|
|
column :expires_at
|
|
column :last_used_at
|
|
column :created_at
|
|
actions
|
|
end
|
|
|
|
filter :name_cont, label: "Name"
|
|
filter :token_prefix_cont, label: "Token prefix"
|
|
filter :expires_at
|
|
filter :last_used_at
|
|
|
|
form do |f|
|
|
owner_class = WarpEngine.config.application_token_owner_class&.safe_constantize
|
|
f.inputs do
|
|
if f.object.new_record?
|
|
if owner_class
|
|
f.input :owner_id, as: :select, label: owner_class.name,
|
|
collection: owner_class.all.map { |o| [ o.try(:email) || o.try(:name) || "##{o.id}", o.id ] },
|
|
include_blank: false
|
|
else
|
|
f.template.concat(f.template.content_tag(:li,
|
|
"application_token_owner_class is not configured — tokens cannot be created.",
|
|
class: "flash flash_error"))
|
|
end
|
|
end
|
|
f.input :name
|
|
f.input :scopes_string, label: "Scopes (comma separated)",
|
|
hint: %(The "update" scope is required for /build/publish, the "upload" scope for /build/upload, the "catalog" scope for a client reading the API. Client tokens are normally issued by device sign-in rather than created here.)
|
|
f.input :unrestricted, hint: "Internal token: exempt from owner isolation (enforce_software_ownership)."
|
|
f.input :expires_at, hint: "Leave empty for a token that never expires."
|
|
end
|
|
f.actions
|
|
end
|
|
|
|
action_item :rotate, only: :show do
|
|
if WarpEngine.ci.configured?
|
|
link_to "Rotate Token", rotate_admin_application_token_path(resource),
|
|
method: :post, data: { confirm: "This will revoke the current token, create a new one, and push it to Woodpecker. Continue?" }
|
|
end
|
|
end
|
|
|
|
member_action :rotate, method: :post do
|
|
result = WarpEngine::SecretSyncService.new.rotate(resource)
|
|
if result[:rotated]
|
|
session[:warp_engine_plain_token] = result[:new_token].plain_token
|
|
redirect_to resource_path(result[:new_token]),
|
|
notice: "Token rotated and synced to #{result.dig(:sync_result, :synced)&.size || 0} pipeline(s)"
|
|
else
|
|
redirect_to resource_path(resource),
|
|
alert: "Rotation failed: #{result[:reason]}"
|
|
end
|
|
end
|
|
|
|
show do
|
|
if (plain = controller.instance_variable_get(:@plain_token))
|
|
panel "⚠️ Token — shown only once, copy it now!" do
|
|
pre plain, style: "font-family:monospace;font-size:14px;padding:8px;background:#fff3cd;user-select:all;"
|
|
end
|
|
end
|
|
attributes_table do
|
|
row :id
|
|
row :name
|
|
row("Token") { |t| code "#{t.token_prefix}… (SHA256 digest stored)" }
|
|
row("Owner") { |t| "#{t.owner_type} ##{t.owner_id} — #{t.owner.try(:email) || t.owner.try(:name)}" }
|
|
row("Scopes") { |t| t.scopes_string }
|
|
row :unrestricted
|
|
row :expires_at
|
|
row :last_used_at
|
|
row :created_at
|
|
row :updated_at
|
|
end
|
|
end
|
|
|
|
controller do
|
|
def create
|
|
create! do |success, _failure|
|
|
success.html do
|
|
session[:warp_engine_plain_token] = resource.plain_token
|
|
|
|
if WarpEngine.ci.configured?
|
|
service = WarpEngine::SecretSyncService.new
|
|
pipelines = service.pipelines_for_token(resource)
|
|
if pipelines.any?
|
|
result = service.provision(resource.plain_token, pipelines: pipelines)
|
|
flash[:notice] = "Token created and synced to #{result[:synced].size} pipeline(s)."
|
|
if result[:failed].any?
|
|
flash[:alert] = "Failed to sync to #{result[:failed].size} pipeline(s)."
|
|
end
|
|
end
|
|
end
|
|
|
|
redirect_to resource_path(resource) and return
|
|
end
|
|
end
|
|
end
|
|
|
|
def show
|
|
@plain_token = session.delete(:warp_engine_plain_token)
|
|
show!
|
|
end
|
|
|
|
def destroy
|
|
if WarpEngine.ci.configured?
|
|
WarpEngine::SecretSyncService.new.deprovision(resource)
|
|
end
|
|
resource.revoke!
|
|
redirect_to collection_path, notice: "Token revoked and Woodpecker secrets cleaned up."
|
|
end
|
|
end
|
|
end
|