mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-12 20:30:05 +01:00
feat: sdk migration in progress
This commit is contained in:
136
.gitignore
vendored
Normal file
136
.gitignore
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/go,react,intellij+all
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=go,react,intellij+all
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
### Intellij+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Intellij+all Patch ###
|
||||||
|
# Ignore everything but code style settings and run configurations
|
||||||
|
# that are supposed to be shared within teams.
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
!.idea/codeStyles
|
||||||
|
!.idea/runConfigurations
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
*.log
|
||||||
|
logs
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/go,react,intellij+all
|
||||||
|
|
||||||
|
.env
|
||||||
|
config.yaml
|
||||||
|
go_*_WhiteSpaceConflict/
|
||||||
|
notes.txt
|
||||||
277
Makefile
Normal file
277
Makefile
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# --- variables ---
|
||||||
|
GOCMD ?= go
|
||||||
|
GOINSTALL := $(GOCMD) install
|
||||||
|
BIN ?= autoglue
|
||||||
|
MAIN ?= main.go
|
||||||
|
UI_DIR ?= ui
|
||||||
|
UI_DEST_DIR ?= internal/web
|
||||||
|
|
||||||
|
# Module path (used for ldflags to internal/version)
|
||||||
|
GIT_HOST ?= github.com
|
||||||
|
GIT_USER ?= glueops
|
||||||
|
MODULE_PATH ?= $(GIT_HOST)/$(GIT_USER)/$(BIN)
|
||||||
|
|
||||||
|
# SDK / module settings (Go)
|
||||||
|
SDK_REPO ?= $(BIN)-sdk # repo name used for module path
|
||||||
|
SDK_OUTDIR ?= sdk/go # output directory (inside repo)
|
||||||
|
SDK_PKG ?= ${BIN} # package name inside the SDK
|
||||||
|
|
||||||
|
UI_SSG_ROUTES ?= /,/login,/docs,/pricing
|
||||||
|
|
||||||
|
# Go versioning (go.mod uses major.minor; you’re on 1.25.3)
|
||||||
|
GO_VERSION ?= 1.25.3
|
||||||
|
|
||||||
|
# SDK / package settings (TypeScript)
|
||||||
|
SDK_TS_OUTDIR ?= sdk/ts
|
||||||
|
SDK_TS_GEN ?= typescript-fetch
|
||||||
|
SDK_TS_NPM_NAME ?= @glueops/$(SDK_REPO)
|
||||||
|
SDK_TS_NPM_VER ?= 0.1.0
|
||||||
|
SDK_TS_DIR := $(abspath $(SDK_TS_OUTDIR))
|
||||||
|
SDK_TS_PROPS ?= supportsES6=true,typescriptThreePlus=true,useSingleRequestParameter=true,withSeparateModelsAndApi=true,modelPropertyNaming=original,enumPropertyNaming=original,useUnionTypes=true
|
||||||
|
SDK_TS_PROPS_FLAGS := $(foreach p,$(subst , ,$(SDK_TS_PROPS)),-p $(p))
|
||||||
|
|
||||||
|
# Path for vendored UI SDK (absolute, path-safe)
|
||||||
|
SDK_TS_UI_OUTDIR ?= ui/src/sdk
|
||||||
|
SDK_TS_UI_DIR := $(abspath $(SDK_TS_UI_OUTDIR))
|
||||||
|
|
||||||
|
SWAG := $(shell command -v swag 2>/dev/null)
|
||||||
|
GMU := $(shell command -v go-mod-upgrade 2>/dev/null)
|
||||||
|
YARN := $(shell command -v yarn 2>/dev/null)
|
||||||
|
NPM := $(shell command -v npm 2>/dev/null)
|
||||||
|
OGC := $(shell command -v openapi-generator-cli 2>/dev/null || command -v openapi-generator 2>/dev/null)
|
||||||
|
BROTLI := $(shell command -v brotli 2>/dev/null)
|
||||||
|
GZIP := $(shell command -v gzip 2>/dev/null)
|
||||||
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
|
# --- version metadata (ldflags) ---
|
||||||
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
|
COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "none")
|
||||||
|
DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
BUILT_BY := $(shell whoami)
|
||||||
|
|
||||||
|
LDFLAGS := -X '$(MODULE_PATH)/internal/version.Version=$(VERSION)' \
|
||||||
|
-X '$(MODULE_PATH)/internal/version.Commit=$(COMMIT)' \
|
||||||
|
-X '$(MODULE_PATH)/internal/version.Date=$(DATE)' \
|
||||||
|
-X '$(MODULE_PATH)/internal/version.BuiltBy=$(BUILT_BY)'
|
||||||
|
|
||||||
|
# --- phony targets ---
|
||||||
|
.PHONY: all prepare ui-install ui-build ui swagger build clean fmt vet tidy upgrade \
|
||||||
|
sdk sdk-go sdk-ts sdk-ts-ui sdk-all worksync wire-sdk-replace help dev ui-compress \
|
||||||
|
print-version
|
||||||
|
|
||||||
|
# --- meta targets ---
|
||||||
|
all: build
|
||||||
|
prepare: fmt vet tidy upgrade
|
||||||
|
|
||||||
|
# --- go hygiene ---
|
||||||
|
fmt:
|
||||||
|
@$(GOCMD) fmt ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@$(GOCMD) vet ./...
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
@$(GOCMD) mod tidy
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
@echo ">> Checking go-mod-upgrade..."
|
||||||
|
@if [ -z "$(GMU)" ]; then \
|
||||||
|
echo "Installing go-mod-upgrade..."; \
|
||||||
|
$(GOINSTALL) github.com/oligot/go-mod-upgrade@latest; \
|
||||||
|
fi
|
||||||
|
@go-mod-upgrade -f || true
|
||||||
|
|
||||||
|
# --- ui ---
|
||||||
|
ui-install:
|
||||||
|
@echo ">> Installing UI deps in $(UI_DIR)..."
|
||||||
|
@if [ -n "$(YARN)" ]; then \
|
||||||
|
cd $(UI_DIR) && yarn install --frozen-lockfile; \
|
||||||
|
elif [ -n "$(NPM)" ]; then \
|
||||||
|
cd $(UI_DIR) && npm ci; \
|
||||||
|
else \
|
||||||
|
echo "Error: neither yarn nor npm is installed." >&2; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui-build: ui-install
|
||||||
|
@echo ">> Building UI in $(UI_DIR)..."
|
||||||
|
@rm -rf $(UI_DEST_DIR)/dist
|
||||||
|
@if [ -n "$(YARN)" ]; then \
|
||||||
|
cd $(UI_DIR) && yarn build; \
|
||||||
|
else \
|
||||||
|
cd $(UI_DIR) && npm run build; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui-compress: ui-build
|
||||||
|
@echo ">> Precompressing assets (brotli + gzip) in $(UI_DEST_DIR)/dist"
|
||||||
|
@if [ -n "$(BROTLI)" ]; then \
|
||||||
|
find "$(UI_DEST_DIR)/dist" -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' \) -print0 | \
|
||||||
|
xargs -0 -I{} brotli -f {}; \
|
||||||
|
else echo "brotli not found; skipping .br"; fi
|
||||||
|
@if [ -n "$(GZIP)" ]; then \
|
||||||
|
find "$(UI_DEST_DIR)/dist" -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' \) -print0 | \
|
||||||
|
xargs -0 -I{} gzip -kf {}; \
|
||||||
|
else echo "gzip not found; skipping .gz"; fi
|
||||||
|
|
||||||
|
ui: ui-compress
|
||||||
|
|
||||||
|
# --- swagger ---
|
||||||
|
swagger:
|
||||||
|
@echo ">> Generating Swagger docs..."
|
||||||
|
@if [ -z "$(SWAG)" ]; then \
|
||||||
|
echo "Installing swag..."; \
|
||||||
|
$(GOINSTALL) github.com/swaggo/swag/cmd/swag@latest; \
|
||||||
|
fi
|
||||||
|
@rm -rf docs/swagger.* docs/docs.go
|
||||||
|
@swag init -g $(MAIN) -o docs
|
||||||
|
|
||||||
|
# --- build ---
|
||||||
|
build: prepare ui swagger sdk-all
|
||||||
|
@echo ">> Building Go binary: $(BIN)"
|
||||||
|
@$(GOCMD) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN) $(MAIN)
|
||||||
|
|
||||||
|
# Handy: print resolved version metadata
|
||||||
|
print-version:
|
||||||
|
@echo "VERSION = $(VERSION)"
|
||||||
|
@echo "COMMIT = $(COMMIT)"
|
||||||
|
@echo "DATE = $(DATE)"
|
||||||
|
@echo "BUILT_BY = $(BUILT_BY)"
|
||||||
|
@echo "LDFLAGS = $(LDFLAGS)"
|
||||||
|
|
||||||
|
# --- development ---
|
||||||
|
dev: ui-install swagger
|
||||||
|
@echo ">> Starting Vite (frontend) and Go API (backend) with dev env..."
|
||||||
|
@cd $(UI_DIR) && \
|
||||||
|
( \
|
||||||
|
if command -v yarn >/dev/null 2>&1; then \
|
||||||
|
yarn dev & \
|
||||||
|
elif command -v npm >/dev/null 2>&1; then \
|
||||||
|
npm run dev & \
|
||||||
|
else \
|
||||||
|
echo "Error: neither yarn nor npm is installed." >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
cd .. && \
|
||||||
|
$(GOCMD) run . serve & \
|
||||||
|
wait \
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- sdk generation (Go) ---
|
||||||
|
sdk-go: swagger
|
||||||
|
@echo ">> Generating Go SDK (module $(GIT_HOST)/$(GIT_USER)/$(SDK_REPO), Go $(GO_VERSION))..."
|
||||||
|
@set -e; \
|
||||||
|
export GO_POST_PROCESS_FILE="gofmt -w"; \
|
||||||
|
if [ -z "$(OGC)" ]; then \
|
||||||
|
if [ -z "$(NPM)" ]; then \
|
||||||
|
echo "Error: npm is required to install openapi-generator-cli." >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "Installing openapi-generator-cli..."; \
|
||||||
|
$(NPM) i -g @openapitools/openapi-generator-cli; \
|
||||||
|
OGC_BIN=openapi-generator-cli; \
|
||||||
|
else \
|
||||||
|
OGC_BIN="$(OGC)"; \
|
||||||
|
fi; \
|
||||||
|
rm -rf "$(SDK_OUTDIR)"; \
|
||||||
|
mkdir -p "$(SDK_OUTDIR)"; \
|
||||||
|
"$$OGC_BIN" generate \
|
||||||
|
--enable-post-process-file \
|
||||||
|
--generate-alias-as-model \
|
||||||
|
-i docs/swagger.json \
|
||||||
|
-g go \
|
||||||
|
-o "$(SDK_OUTDIR)" \
|
||||||
|
--additional-properties=packageName=$(SDK_PKG) \
|
||||||
|
--git-host "$(GIT_HOST)" \
|
||||||
|
--git-user-id "$(GIT_USER)" \
|
||||||
|
--git-repo-id "$(SDK_REPO)"; \
|
||||||
|
cd "$(SDK_OUTDIR)"; \
|
||||||
|
$(GOCMD) mod edit -go=$(GO_VERSION); \
|
||||||
|
$(GOCMD) mod tidy
|
||||||
|
|
||||||
|
# --- sdk generation (TypeScript) ---
|
||||||
|
sdk-ts: swagger
|
||||||
|
@set -e; \
|
||||||
|
if [ -z "$(OGC)" ]; then \
|
||||||
|
if [ -z "$(NPM)" ]; then echo "Error: npm is required to install openapi-generator-cli." >&2; exit 1; fi; \
|
||||||
|
echo "Installing openapi-generator-cli..."; \
|
||||||
|
$(NPM) i -g @openapitools/openapi-generator-cli; \
|
||||||
|
OGC_BIN=openapi-generator-cli; \
|
||||||
|
else \
|
||||||
|
OGC_BIN="$(OGC)"; \
|
||||||
|
fi; \
|
||||||
|
rm -rf "$(SDK_TS_DIR)"; \
|
||||||
|
mkdir -p "$(SDK_TS_DIR)"; \
|
||||||
|
"$$OGC_BIN" generate \
|
||||||
|
-i docs/swagger.json \
|
||||||
|
-g "$(SDK_TS_GEN)" \
|
||||||
|
-o "$(SDK_TS_DIR)" \
|
||||||
|
-p npmName=$(SDK_TS_NPM_NAME) \
|
||||||
|
-p npmVersion=$(SDK_TS_NPM_VER) \
|
||||||
|
$(SDK_TS_PROPS_FLAGS); \
|
||||||
|
if [ ! -d "$(SDK_TS_DIR)" ]; then \
|
||||||
|
echo "Generation failed: $(SDK_TS_DIR) not found." >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
if command -v npx >/dev/null 2>&1; then \
|
||||||
|
echo ">> Prettier: formatting generated TS SDK"; \
|
||||||
|
cd "$(SDK_TS_DIR)" && npx --yes prettier -w . || true; \
|
||||||
|
fi; \
|
||||||
|
echo ">> Installing & building TS SDK in $(SDK_TS_DIR)"; \
|
||||||
|
if command -v yarn >/dev/null 2>&1; then \
|
||||||
|
cd "$(SDK_TS_DIR)" && yarn install --frozen-lockfile || true; \
|
||||||
|
cd "$(SDK_TS_DIR)" && yarn build || true; \
|
||||||
|
elif command -v npm >/dev/null 2>&1; then \
|
||||||
|
cd "$(SDK_TS_DIR)" && npm ci || npm install || true; \
|
||||||
|
cd "$(SDK_TS_DIR)" && npm run build || true; \
|
||||||
|
else \
|
||||||
|
echo "Warning: neither yarn nor npm is installed; skipping install/build for TS SDK."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- sdk generation (TypeScript into UI/src) ---
|
||||||
|
sdk-ts-ui: swagger
|
||||||
|
@echo ">> Generating TypeScript SDK directly into UI source: $(SDK_TS_UI_DIR)"
|
||||||
|
@set -e; \
|
||||||
|
if [ -z "$(OGC)" ]; then \
|
||||||
|
if [ -z "$(NPM)" ]; then \
|
||||||
|
echo "Error: npm is required to install openapi-generator-cli." >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "Installing openapi-generator-cli..."; \
|
||||||
|
$(NPM) i -g @openapitools/openapi-generator-cli; \
|
||||||
|
OGC_BIN=openapi-generator-cli; \
|
||||||
|
else \
|
||||||
|
OGC_BIN="$(OGC)"; \
|
||||||
|
fi; \
|
||||||
|
rm -rf "$(SDK_TS_UI_DIR)"; \
|
||||||
|
mkdir -p "$(SDK_TS_UI_DIR)"; \
|
||||||
|
"$$OGC_BIN" generate \
|
||||||
|
-i docs/swagger.json \
|
||||||
|
-g typescript-fetch \
|
||||||
|
-o "$(SDK_TS_UI_DIR)" \
|
||||||
|
-p npmName=$(SDK_TS_NPM_NAME) \
|
||||||
|
-p npmVersion=$(SDK_TS_NPM_VER) \
|
||||||
|
$(SDK_TS_PROPS_FLAGS); \
|
||||||
|
# --- move src/* up one level ---
|
||||||
|
@if [ -d "$(SDK_TS_UI_DIR)/src" ]; then \
|
||||||
|
mv "$(SDK_TS_UI_DIR)/src/"* "$(SDK_TS_UI_DIR)/"; \
|
||||||
|
rm -rf "$(SDK_TS_UI_DIR)/src"; \
|
||||||
|
fi; \
|
||||||
|
rm -f "$(SDK_TS_UI_DIR)/package.json" "$(SDK_TS_UI_DIR)/tsconfig.json" "$(SDK_TS_UI_DIR)/README.md"
|
||||||
|
|
||||||
|
# convenience
|
||||||
|
sdk-all: sdk-go sdk-ts sdk-ts-ui
|
||||||
|
sdk: sdk-go
|
||||||
|
|
||||||
|
# --- clean/help ---
|
||||||
|
clean:
|
||||||
|
@echo ">> Cleaning artifacts..."
|
||||||
|
@rm -rf "$(BIN)" docs/swagger.* docs/docs.go $(UI_DEST_DIR)/dist $(UI_DIR)/dist $(UI_DIR)/node_modules "$(SDK_OUTDIR)" "$(SDK_TS_OUTDIR)"
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " build - fmt, vet, tidy, upgrade, build UI, generate Swagger, build Go binary (with ldflags)"
|
||||||
|
@echo " ui - build the Vite UI and copy to $(UI_DEST_DIR)/dist (with compression)"
|
||||||
|
@echo " swagger - (re)generate Swagger docs using swag"
|
||||||
|
@echo " sdk-go (sdk) - generate Go SDK with correct module path and Go version"
|
||||||
|
@echo " sdk-ts - generate TypeScript SDK (typescript-fetch) with package.json"
|
||||||
|
@echo " sdk-ts-ui - generate TypeScript SDK directly into ui/src for inline consumption"
|
||||||
|
@echo " sdk-all - generate both Go and TypeScript SDKs"
|
||||||
|
@echo " dev - run Vite UI dev server + Go API"
|
||||||
|
@echo " clean - remove binary, Swagger outputs, UI dist, and SDKs"
|
||||||
|
@echo " prepare - fmt, vet, tidy, upgrade deps"
|
||||||
|
@echo " print-version - show computed ldflags values"
|
||||||
83
cmd/encryption.go
Normal file
83
cmd/encryption.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/app"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rotateMasterCmd = &cobra.Command{
|
||||||
|
Use: "rotate-master",
|
||||||
|
Short: "Generate and activate a new master encryption key",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
rt := app.NewRuntime()
|
||||||
|
db := rt.DB
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
return fmt.Errorf("generating random key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(key)
|
||||||
|
|
||||||
|
if err := db.Model(&models.MasterKey{}).
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Update("is_active", false).Error; err != nil {
|
||||||
|
return fmt.Errorf("deactivating previous key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&models.MasterKey{
|
||||||
|
Key: encoded,
|
||||||
|
IsActive: true,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("creating new master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Master key rotated successfully")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var createMasterCmd = &cobra.Command{
|
||||||
|
Use: "create-master",
|
||||||
|
Short: "Generate and activate a new master encryption key",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
rt := app.NewRuntime()
|
||||||
|
db := rt.DB
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
return fmt.Errorf("generating random key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(key)
|
||||||
|
|
||||||
|
if err := db.Create(&models.MasterKey{
|
||||||
|
Key: encoded,
|
||||||
|
IsActive: true,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("creating master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Master key created successfully")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptCmd = &cobra.Command{
|
||||||
|
Use: "encrypt",
|
||||||
|
Short: "Manage autoglue encryption keys",
|
||||||
|
Long: "Manage autoglue master encryption keys used for securing data.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
encryptCmd.AddCommand(rotateMasterCmd)
|
||||||
|
encryptCmd.AddCommand(createMasterCmd)
|
||||||
|
rootCmd.AddCommand(encryptCmd)
|
||||||
|
}
|
||||||
81
cmd/keys_generate.go
Normal file
81
cmd/keys_generate.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/app"
|
||||||
|
"github.com/glueops/autoglue/internal/keys"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
alg string
|
||||||
|
rsaBits int
|
||||||
|
kidFlag string
|
||||||
|
nbfStr string
|
||||||
|
expStr string
|
||||||
|
)
|
||||||
|
|
||||||
|
var keysCmd = &cobra.Command{
|
||||||
|
Use: "keys",
|
||||||
|
Short: "Manage JWT signing keys",
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysGenCmd = &cobra.Command{
|
||||||
|
Use: "generate",
|
||||||
|
Short: "Generate and store a new signing key",
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
rt := app.NewRuntime()
|
||||||
|
|
||||||
|
var nbfPtr, expPtr *time.Time
|
||||||
|
if nbfStr != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, nbfStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nbfPtr = &t
|
||||||
|
}
|
||||||
|
if expStr != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, expStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expPtr = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, err := keys.GenerateAndStore(rt.DB, rt.Cfg.JWTPrivateEncKey, keys.GenOpts{
|
||||||
|
Alg: alg,
|
||||||
|
Bits: rsaBits,
|
||||||
|
KID: kidFlag,
|
||||||
|
NBF: nbfPtr,
|
||||||
|
EXP: expPtr,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("created signing key\n")
|
||||||
|
fmt.Printf(" kid: %s\n", rec.Kid)
|
||||||
|
fmt.Printf(" alg: %s\n", rec.Alg)
|
||||||
|
fmt.Printf(" active: %v\n", rec.IsActive)
|
||||||
|
if rec.NotBefore != nil {
|
||||||
|
fmt.Printf(" nbf: %s\n", rec.NotBefore.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if rec.ExpiresAt != nil {
|
||||||
|
fmt.Printf(" exp: %s\n", rec.ExpiresAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(keysCmd)
|
||||||
|
keysCmd.AddCommand(keysGenCmd)
|
||||||
|
|
||||||
|
keysGenCmd.Flags().StringVarP(&alg, "alg", "a", "EdDSA", "Signing alg: EdDSA|RS256|RS384|RS512")
|
||||||
|
keysGenCmd.Flags().IntVarP(&rsaBits, "bits", "b", 3072, "RSA key size (when alg is RS*)")
|
||||||
|
keysGenCmd.Flags().StringVarP(&kidFlag, "kid", "k", "", "Key ID (optional; auto if empty)")
|
||||||
|
keysGenCmd.Flags().StringVarP(&nbfStr, "nbf", "n", "", "Not Before (RFC3339)")
|
||||||
|
keysGenCmd.Flags().StringVarP(&expStr, "exp", "e", "", "Expires At (RFC3339)")
|
||||||
|
}
|
||||||
33
cmd/root.go
Normal file
33
cmd/root.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "autoglue",
|
||||||
|
Short: "Autoglue Kubernetes Cluster Management",
|
||||||
|
Long: "autoglue is used to manage the lifecycle of kubernetes clusters on GlueOps supported cloud providers",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
err := serveCmd.RunE(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = cmd.Help()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize()
|
||||||
|
}
|
||||||
94
cmd/serve.go
Normal file
94
cmd/serve.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api"
|
||||||
|
"github.com/glueops/autoglue/internal/app"
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Start API server",
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
rt := app.NewRuntime()
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(60 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
for range t.C {
|
||||||
|
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
r := api.NewRouter(rt.DB)
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: TimeoutExceptUpgrades(r, 60*time.Second, "request timed out"), // global safety
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("🚀 API running on http://%s (ui.dev=%v)\n", addr, cfg.UIDev)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
fmt.Println("\n⏳ Shutting down...")
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return srv.Shutdown(shutdownCtx)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeoutExceptUpgrades(next http.Handler, d time.Duration, msg string) http.Handler {
|
||||||
|
timeout := http.TimeoutHandler(next, d, msg)
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// If this is an upgrade (e.g., websocket), don't wrap.
|
||||||
|
if isUpgrade(r) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeout.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUpgrade(r *http.Request) bool {
|
||||||
|
// Connection: Upgrade, Upgrade: websocket
|
||||||
|
if strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
20
cmd/version.go
Normal file
20
cmd/version.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show version information",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Println(version.Info())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
}
|
||||||
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
services:
|
||||||
|
autoglue:
|
||||||
|
# image: ghcr.io/glueops/autoglue:latest
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
expose:
|
||||||
|
- 8080
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
AUTOGLUE_DATABASE_DSN: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
||||||
|
AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
build:
|
||||||
|
context: postgres
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: $DB_USER
|
||||||
|
POSTGRES_PASSWORD: $DB_PASSWORD
|
||||||
|
POSTGRES_DB: $DB_NAME
|
||||||
|
expose:
|
||||||
|
- 5432
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
pgweb:
|
||||||
|
image: sosedoff/pgweb@sha256:8f1ed22e10c9da0912169b98b62ddc54930dc39a5ae07b0f1354d2a93d44c6ed
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
links:
|
||||||
|
- postgres:postgres
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
PGWEB_DATABASE_URL: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: axllent/mailpit@sha256:6abc8e633df15eaf785cfcf38bae48e66f64beecdc03121e249d0f9ec15f0707
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "1025:1025"
|
||||||
|
- "8025:8025"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
26
docs/docs.go
Normal file
26
docs/docs.go
Normal file
File diff suppressed because one or more lines are too long
9
docs/efs.go
Normal file
9
docs/efs.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package docs
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed swagger.json
|
||||||
|
var SwaggerJSON []byte
|
||||||
|
|
||||||
|
//go:embed swagger.yaml
|
||||||
|
var SwaggerYAML []byte
|
||||||
1
docs/swagger.json
Normal file
1
docs/swagger.json
Normal file
File diff suppressed because one or more lines are too long
1731
docs/swagger.yaml
Normal file
1731
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
70
go.mod
Normal file
70
go.mod
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
module github.com/glueops/autoglue
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
|
github.com/go-chi/cors v1.2.2
|
||||||
|
github.com/go-chi/httprate v0.15.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/spf13/cobra v1.10.1
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||||
|
github.com/swaggo/swag/v2 v2.0.0-rc4
|
||||||
|
golang.org/x/crypto v0.35.0
|
||||||
|
golang.org/x/oauth2 v0.32.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/datatypes v1.2.7
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.9 // indirect
|
||||||
|
github.com/go-openapi/swag v0.22.3 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||||
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
|
github.com/swaggo/swag v1.8.1 // indirect
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
|
)
|
||||||
236
go.sum
Normal file
236
go.sum
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||||
|
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
|
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
|
||||||
|
github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||||
|
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||||
|
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||||
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||||
|
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA=
|
||||||
|
github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg=
|
||||||
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
|
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||||
|
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
||||||
|
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||||
|
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||||
|
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||||
|
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
||||||
|
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
|
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||||
|
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
|
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||||
|
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
161
internal/api/httpmiddleware/auth.go
Normal file
161
internal/api/httpmiddleware/auth.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package httpmiddleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware authenticates either a user principal (JWT, user API key, app key/secret)
|
||||||
|
// or an org principal (org key/secret). If requireOrg is true, the request must have
|
||||||
|
// an organization resolved; otherwise org is optional.
|
||||||
|
//
|
||||||
|
// Org resolution order for user principals (when requireOrg == true):
|
||||||
|
// 1. X-Org-ID header (UUID)
|
||||||
|
// 2. chi URL param {id} (useful under /orgs/{id}/... routers)
|
||||||
|
// 3. single-membership fallback (exactly one membership)
|
||||||
|
//
|
||||||
|
// If none resolves, respond with org_required.
|
||||||
|
func AuthMiddleware(db *gorm.DB, requireOrg bool) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var user *models.User
|
||||||
|
var org *models.Organization
|
||||||
|
var roles []string
|
||||||
|
|
||||||
|
// --- 1) Authenticate principal ---
|
||||||
|
// Prefer org principal if explicit machine access is provided.
|
||||||
|
if orgKey := r.Header.Get("X-ORG-KEY"); orgKey != "" {
|
||||||
|
secret := r.Header.Get("X-ORG-SECRET")
|
||||||
|
org = auth.ValidateOrgKeyPair(orgKey, secret, db)
|
||||||
|
if org == nil {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "invalid org credentials")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// org principal implies machine role
|
||||||
|
roles = []string{"org:machine"}
|
||||||
|
} else {
|
||||||
|
// User principals
|
||||||
|
if ah := r.Header.Get("Authorization"); strings.HasPrefix(ah, "Bearer ") {
|
||||||
|
user = auth.ValidateJWT(ah[7:], db)
|
||||||
|
} else if apiKey := r.Header.Get("X-API-KEY"); apiKey != "" {
|
||||||
|
user = auth.ValidateAPIKey(apiKey, db)
|
||||||
|
} else if appKey := r.Header.Get("X-APP-KEY"); appKey != "" {
|
||||||
|
secret := r.Header.Get("X-APP-SECRET")
|
||||||
|
user = auth.ValidateAppKeyPair(appKey, secret, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "invalid credentials")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2) Resolve organization (user principal) ---
|
||||||
|
// A) Try X-Org-ID if present
|
||||||
|
if s := r.Header.Get("X-Org-ID"); s != "" {
|
||||||
|
oid, err := uuid.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_org_id", "X-Org-ID must be a UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var o models.Organization
|
||||||
|
if err := db.First(&o, "id = ?", oid).Error; err != nil {
|
||||||
|
// Header provided but org not found
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "org_forbidden", "organization not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Verify membership
|
||||||
|
if !userIsMember(db, user.ID, o.ID) {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "org_forbidden", "user is not a member of specified org")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
org = &o
|
||||||
|
}
|
||||||
|
|
||||||
|
// B) If still no org and requireOrg==true, try chi URL param {id}
|
||||||
|
if org == nil && requireOrg {
|
||||||
|
if sid := chi.URLParam(r, "id"); sid != "" {
|
||||||
|
if oid, err := uuid.Parse(sid); err == nil {
|
||||||
|
var o models.Organization
|
||||||
|
if err := db.First(&o, "id = ?", oid).Error; err == nil && userIsMember(db, user.ID, o.ID) {
|
||||||
|
org = &o
|
||||||
|
} else {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "org_forbidden", "user is not a member of specified org")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Single-membership fallback (only if requireOrg==true and still nil)
|
||||||
|
if org == nil && requireOrg {
|
||||||
|
var ms []models.Membership
|
||||||
|
if err := db.Where("user_id = ?", user.ID).Find(&ms).Error; err == nil && len(ms) == 1 {
|
||||||
|
var o models.Organization
|
||||||
|
if err := db.First(&o, "id = ?", ms[0].OrganizationID).Error; err == nil {
|
||||||
|
org = &o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D) Final check
|
||||||
|
if requireOrg && org == nil {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "org_required", "specify X-Org-ID or use an endpoint that does not require org")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate roles if an org was resolved (optional for org-optional endpoints)
|
||||||
|
if org != nil {
|
||||||
|
roles = userRolesInOrg(db, user.ID, org.ID)
|
||||||
|
if len(roles) == 0 {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "forbidden", "no roles in organization")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3) Attach to context and proceed ---
|
||||||
|
ctx := r.Context()
|
||||||
|
if user != nil {
|
||||||
|
ctx = WithUser(ctx, user)
|
||||||
|
}
|
||||||
|
if org != nil {
|
||||||
|
ctx = WithOrg(ctx, org)
|
||||||
|
}
|
||||||
|
if roles != nil {
|
||||||
|
ctx = WithRoles(ctx, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userIsMember(db *gorm.DB, userID, orgID uuid.UUID) bool {
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.Membership{}).
|
||||||
|
Where("user_id = ? AND organization_id = ?", userID, orgID).
|
||||||
|
Count(&count)
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func userRolesInOrg(db *gorm.DB, userID, orgID uuid.UUID) []string {
|
||||||
|
var m models.Membership
|
||||||
|
if err := db.Where("user_id = ? AND organization_id = ?", userID, orgID).First(&m).Error; err == nil {
|
||||||
|
switch m.Role {
|
||||||
|
case "owner":
|
||||||
|
return []string{"role:owner", "role:admin", "role:member"}
|
||||||
|
case "admin":
|
||||||
|
return []string{"role:admin", "role:member"}
|
||||||
|
default:
|
||||||
|
return []string{"role:member"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
internal/api/httpmiddleware/context.go
Normal file
45
internal/api/httpmiddleware/context.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package httpmiddleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxUserKey ctxKey = "ctx_user"
|
||||||
|
ctxOrgKey ctxKey = "ctx_org"
|
||||||
|
ctxRolesKey ctxKey = "ctx_roles" // []string, user roles in current org
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithUser(ctx context.Context, u *models.User) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxUserKey, u)
|
||||||
|
}
|
||||||
|
func WithOrg(ctx context.Context, o *models.Organization) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxOrgKey, o)
|
||||||
|
}
|
||||||
|
func WithRoles(ctx context.Context, roles []string) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxRolesKey, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserFrom(ctx context.Context) (*models.User, bool) {
|
||||||
|
u, ok := ctx.Value(ctxUserKey).(*models.User)
|
||||||
|
return u, ok && u != nil
|
||||||
|
}
|
||||||
|
func OrgFrom(ctx context.Context) (*models.Organization, bool) {
|
||||||
|
o, ok := ctx.Value(ctxOrgKey).(*models.Organization)
|
||||||
|
return o, ok && o != nil
|
||||||
|
}
|
||||||
|
func OrgIDFrom(ctx context.Context) (uuid.UUID, bool) {
|
||||||
|
if o, ok := OrgFrom(ctx); ok {
|
||||||
|
return o.ID, true
|
||||||
|
}
|
||||||
|
return uuid.Nil, false
|
||||||
|
}
|
||||||
|
func RolesFrom(ctx context.Context) ([]string, bool) {
|
||||||
|
r, ok := ctx.Value(ctxRolesKey).([]string)
|
||||||
|
return r, ok && r != nil
|
||||||
|
}
|
||||||
45
internal/api/httpmiddleware/rbac.go
Normal file
45
internal/api/httpmiddleware/rbac.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package httpmiddleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequireRole(minRole string) func(http.Handler) http.Handler {
|
||||||
|
// order: owner > admin > member
|
||||||
|
rank := map[string]int{
|
||||||
|
"role:member": 1,
|
||||||
|
"role:admin": 2,
|
||||||
|
"role:owner": 3,
|
||||||
|
"org:machine": 2,
|
||||||
|
"org:machine:ro": 1,
|
||||||
|
}
|
||||||
|
need := map[string]bool{
|
||||||
|
"member": true, "admin": true, "owner": true,
|
||||||
|
}
|
||||||
|
if !need[minRole] {
|
||||||
|
minRole = "member"
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
roles, ok := RolesFrom(r.Context())
|
||||||
|
if !ok || len(roles) == 0 {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "forbidden", "no roles in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
max := 0
|
||||||
|
for _, ro := range roles {
|
||||||
|
if rank[ro] > max {
|
||||||
|
max = rank[ro]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if max < rank["role:"+minRole] {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "forbidden", "insufficient role")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
35
internal/api/mw_logger.go
Normal file
35
internal/api/mw_logger.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func zeroLogMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
|
||||||
|
dur := time.Since(start)
|
||||||
|
ev := log.Info()
|
||||||
|
if ww.Status() >= 500 {
|
||||||
|
ev = log.Error()
|
||||||
|
}
|
||||||
|
ev.
|
||||||
|
Str("remote_ip", r.RemoteAddr).
|
||||||
|
Str("request_id", middleware.GetReqID(r.Context())).
|
||||||
|
Str("method", r.Method).
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Int("status", ww.Status()).
|
||||||
|
Int("bytes", ww.BytesWritten()).
|
||||||
|
Dur("duration", dur).
|
||||||
|
Msg("http_request")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
63
internal/api/mw_security.go
Normal file
63
internal/api/mw_security.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SecurityHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// HSTS (enable only over TLS/behind HTTPS)
|
||||||
|
// HSTS only when not in dev and over TLS/behind a proxy that terminates TLS
|
||||||
|
if !config.IsDev() {
|
||||||
|
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("X-Frame-Options", "DENY")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=(), interest-cohort=()")
|
||||||
|
|
||||||
|
if config.IsDev() {
|
||||||
|
// --- Relaxed CSP for Vite dev server & Google Fonts ---
|
||||||
|
// Allows inline/eval for React Refresh preamble, HMR websocket, and fonts.
|
||||||
|
// Tighten these as you move to prod or self-host fonts.
|
||||||
|
w.Header().Set("Content-Security-Policy", strings.Join([]string{
|
||||||
|
"default-src 'self'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
// Vite dev & inline preamble/eval:
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:5173",
|
||||||
|
// allow dev style + Google Fonts
|
||||||
|
"style-src 'self' 'unsafe-inline' http://localhost:5173 https://fonts.googleapis.com",
|
||||||
|
"img-src 'self' data: blob:",
|
||||||
|
// Google font files
|
||||||
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
|
// HMR connections
|
||||||
|
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
}, "; "))
|
||||||
|
} else {
|
||||||
|
// --- Strict CSP for production ---
|
||||||
|
// If you keep using Google Fonts in prod, add:
|
||||||
|
// style-src ... https://fonts.googleapis.com
|
||||||
|
// font-src ... https://fonts.gstatic.com
|
||||||
|
// Recommended: self-host fonts in prod and keep these tight.
|
||||||
|
w.Header().Set("Content-Security-Policy", strings.Join([]string{
|
||||||
|
"default-src 'self'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"script-src 'self'",
|
||||||
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
|
"img-src 'self' data: blob:",
|
||||||
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
|
"connect-src 'self'",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
}, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
195
internal/api/routes.go
Normal file
195
internal/api/routes.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
httpPprof "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/docs"
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers"
|
||||||
|
"github.com/glueops/autoglue/internal/web"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"github.com/go-chi/httprate"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRouter(db *gorm.DB) http.Handler {
|
||||||
|
zerolog.TimeFieldFormat = time.RFC3339
|
||||||
|
|
||||||
|
l := log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"})
|
||||||
|
log.Logger = l
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.RequestID)
|
||||||
|
r.Use(middleware.RealIP)
|
||||||
|
r.Use(zeroLogMiddleware())
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(SecurityHeaders)
|
||||||
|
r.Use(requestBodyLimit(10 << 20))
|
||||||
|
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
||||||
|
|
||||||
|
allowed := getAllowedOrigins()
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: allowed,
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{
|
||||||
|
"Content-Type",
|
||||||
|
"Authorization",
|
||||||
|
"X-Org-ID",
|
||||||
|
"X-API-KEY",
|
||||||
|
"X-ORG-KEY",
|
||||||
|
"X-ORG-SECRET",
|
||||||
|
},
|
||||||
|
ExposedHeaders: []string{"Link"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 600,
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Use(middleware.AllowContentType("application/json"))
|
||||||
|
|
||||||
|
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||||
|
r.Route("/api", func(api chi.Router) {
|
||||||
|
api.Route("/v1", func(v1 chi.Router) {
|
||||||
|
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||||
|
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
||||||
|
|
||||||
|
// Also serving a versioned JWKS for swagger, which uses BasePath
|
||||||
|
v1.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||||
|
|
||||||
|
v1.Route("/auth", func(a chi.Router) {
|
||||||
|
a.Post("/{provider}/start", handlers.AuthStart(db))
|
||||||
|
a.Get("/{provider}/callback", handlers.AuthCallback(db))
|
||||||
|
a.Post("/refresh", handlers.Refresh(db))
|
||||||
|
a.Post("/logout", handlers.Logout(db))
|
||||||
|
})
|
||||||
|
|
||||||
|
v1.Route("/me", func(me chi.Router) {
|
||||||
|
me.Use(authUser)
|
||||||
|
|
||||||
|
me.Get("/", handlers.GetMe(db))
|
||||||
|
me.Patch("/", handlers.UpdateMe(db))
|
||||||
|
|
||||||
|
me.Get("/api-keys", handlers.ListUserAPIKeys(db))
|
||||||
|
me.Post("/api-keys", handlers.CreateUserAPIKey(db))
|
||||||
|
me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db))
|
||||||
|
})
|
||||||
|
|
||||||
|
v1.Route("/orgs", func(o chi.Router) {
|
||||||
|
o.Use(authUser)
|
||||||
|
o.Get("/", handlers.ListMyOrgs(db))
|
||||||
|
o.Post("/", handlers.CreateOrg(db))
|
||||||
|
|
||||||
|
o.Group(func(og chi.Router) {
|
||||||
|
og.Use(authOrg)
|
||||||
|
og.Get("/{id}", handlers.GetOrg(db))
|
||||||
|
og.Patch("/{id}", handlers.UpdateOrg(db))
|
||||||
|
og.Delete("/{id}", handlers.DeleteOrg(db))
|
||||||
|
|
||||||
|
// members
|
||||||
|
og.Get("/{id}/members", handlers.ListMembers(db))
|
||||||
|
og.Post("/{id}/members", handlers.AddOrUpdateMember(db))
|
||||||
|
og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db))
|
||||||
|
|
||||||
|
// org-scoped key/secret pair
|
||||||
|
og.Get("/{id}/api-keys", handlers.ListOrgKeys(db))
|
||||||
|
og.Post("/{id}/api-keys", handlers.CreateOrgKey(db))
|
||||||
|
og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
v1.Route("/ssh", func(s chi.Router) {
|
||||||
|
s.Use(authOrg)
|
||||||
|
s.Get("/", handlers.ListPublicSshKeys(db))
|
||||||
|
s.Post("/", handlers.CreateSSHKey(db))
|
||||||
|
s.Get("/{id}", handlers.GetSSHKey(db))
|
||||||
|
s.Delete("/{id}", handlers.DeleteSSHKey(db))
|
||||||
|
s.Get("/{id}/download", handlers.DownloadSSHKey(db))
|
||||||
|
})
|
||||||
|
|
||||||
|
v1.Route("/servers", func(s chi.Router) {
|
||||||
|
s.Use(authOrg)
|
||||||
|
s.Get("/", handlers.ListServers(db))
|
||||||
|
s.Post("/", handlers.CreateServer(db))
|
||||||
|
s.Get("/{id}", handlers.GetServer(db))
|
||||||
|
s.Patch("/{id}", handlers.UpdateServer(db))
|
||||||
|
s.Delete("/{id}", handlers.DeleteServer(db))
|
||||||
|
})
|
||||||
|
|
||||||
|
v1.Route("/taints", func(s chi.Router) {
|
||||||
|
s.Use(authOrg)
|
||||||
|
s.Get("/", handlers.ListTaints(db))
|
||||||
|
s.Post("/", handlers.CreateTaint(db))
|
||||||
|
s.Get("/{id}", handlers.GetTaint(db))
|
||||||
|
s.Patch("/{id}", handlers.UpdateTaint(db))
|
||||||
|
s.Delete("/{id}", handlers.DeleteTaint(db))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if config.IsDebug() {
|
||||||
|
r.Route("/debug/pprof", func(pr chi.Router) {
|
||||||
|
pr.Get("/", httpPprof.Index)
|
||||||
|
pr.Get("/cmdline", httpPprof.Cmdline)
|
||||||
|
pr.Get("/profile", httpPprof.Profile)
|
||||||
|
pr.Get("/symbol", httpPprof.Symbol)
|
||||||
|
pr.Get("/trace", httpPprof.Trace)
|
||||||
|
|
||||||
|
pr.Handle("/allocs", httpPprof.Handler("allocs"))
|
||||||
|
pr.Handle("/block", httpPprof.Handler("block"))
|
||||||
|
pr.Handle("/goroutine", httpPprof.Handler("goroutine"))
|
||||||
|
pr.Handle("/heap", httpPprof.Handler("heap"))
|
||||||
|
pr.Handle("/mutex", httpPprof.Handler("mutex"))
|
||||||
|
pr.Handle("/threadcreate", httpPprof.Handler("threadcreate"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IsSwaggerEnabled() {
|
||||||
|
r.Get("/swagger/*", httpSwagger.Handler(
|
||||||
|
httpSwagger.URL("swagger.json"),
|
||||||
|
))
|
||||||
|
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||||
|
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IsUIDev() {
|
||||||
|
fmt.Println("Running in development mode")
|
||||||
|
// Dev: isolate proxy from chi middlewares so WS upgrade can hijack.
|
||||||
|
proxy, err := web.DevProxy("http://localhost:5173")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("dev proxy init failed")
|
||||||
|
return r // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
// Send API/Swagger/pprof to chi
|
||||||
|
mux.Handle("/api/", r)
|
||||||
|
mux.Handle("/api", r)
|
||||||
|
mux.Handle("/swagger/", r)
|
||||||
|
mux.Handle("/debug/pprof/", r)
|
||||||
|
// Everything else (/, /brand-preview, assets) → proxy (no middlewares)
|
||||||
|
mux.Handle("/", proxy)
|
||||||
|
|
||||||
|
return mux
|
||||||
|
} else {
|
||||||
|
fmt.Println("Running in production mode")
|
||||||
|
if h, err := web.SPAHandler(); err == nil {
|
||||||
|
r.NotFound(h.ServeHTTP)
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).Msg("spa handler init failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
45
internal/api/utils.go
Normal file
45
internal/api/utils.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func requestBodyLimit(maxBytes int64) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllowedOrigins() []string {
|
||||||
|
if v := os.Getenv("CORS_ALLOWED_ORIGINS"); v != "" {
|
||||||
|
parts := strings.Split(v, ",")
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
s := strings.TrimSpace(p)
|
||||||
|
if s != "" {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Defaults (dev)
|
||||||
|
return []string{
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:8080",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveSwaggerFromEmbed(data []byte, contentType string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
internal/app/runtime.go
Normal file
46
internal/app/runtime.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
"github.com/glueops/autoglue/internal/db"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runtime struct {
|
||||||
|
Cfg config.Config
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRuntime() *Runtime {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
d := db.Open(cfg.DbURL)
|
||||||
|
|
||||||
|
err = db.Run(d,
|
||||||
|
&models.MasterKey{},
|
||||||
|
&models.SigningKey{},
|
||||||
|
&models.User{},
|
||||||
|
&models.Organization{},
|
||||||
|
&models.Account{},
|
||||||
|
&models.Membership{},
|
||||||
|
&models.APIKey{},
|
||||||
|
&models.UserEmail{},
|
||||||
|
&models.RefreshToken{},
|
||||||
|
&models.OrganizationKey{},
|
||||||
|
&models.SshKey{},
|
||||||
|
&models.Server{},
|
||||||
|
&models.Taint{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
|
}
|
||||||
|
return &Runtime{
|
||||||
|
Cfg: cfg,
|
||||||
|
DB: d,
|
||||||
|
}
|
||||||
|
}
|
||||||
38
internal/auth/hash.go
Normal file
38
internal/auth/hash.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SHA256Hex(s string) string {
|
||||||
|
sum := sha256.Sum256([]byte(s))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var argonParams = &argon2id.Params{
|
||||||
|
Memory: 64 * 1024, // 64MB
|
||||||
|
Iterations: 3,
|
||||||
|
Parallelism: 2,
|
||||||
|
SaltLength: 16,
|
||||||
|
KeyLength: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashSecretArgon2id(plain string) (string, error) {
|
||||||
|
return argon2id.CreateHash(plain, argonParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifySecretArgon2id(encodedHash, plain string) (bool, error) {
|
||||||
|
if encodedHash == "" {
|
||||||
|
return false, errors.New("empty hash")
|
||||||
|
}
|
||||||
|
return argon2id.ComparePasswordAndHash(plain, encodedHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotExpired(expiresAt *time.Time) bool {
|
||||||
|
return expiresAt == nil || time.Now().Before(*expiresAt)
|
||||||
|
}
|
||||||
42
internal/auth/issue.go
Normal file
42
internal/auth/issue.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomToken(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// URL-safe, no padding
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueUserAPIKey creates a single-token user API key (X-API-KEY)
|
||||||
|
func IssueUserAPIKey(db *gorm.DB, userID uuid.UUID, name string, ttl *time.Duration) (plaintext string, rec models.APIKey, err error) {
|
||||||
|
plaintext, err = randomToken(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", models.APIKey{}, err
|
||||||
|
}
|
||||||
|
rec = models.APIKey{
|
||||||
|
Name: name,
|
||||||
|
Scope: "user",
|
||||||
|
UserID: &userID,
|
||||||
|
KeyHash: SHA256Hex(plaintext), // deterministic lookup
|
||||||
|
}
|
||||||
|
if ttl != nil {
|
||||||
|
ex := time.Now().Add(*ttl)
|
||||||
|
rec.ExpiresAt = &ex
|
||||||
|
}
|
||||||
|
if err = db.Create(&rec).Error; err != nil {
|
||||||
|
return "", models.APIKey{}, err
|
||||||
|
}
|
||||||
|
return plaintext, rec, nil
|
||||||
|
}
|
||||||
71
internal/auth/jwks_export.go
Normal file
71
internal/auth/jwks_export.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// base64url (no padding)
|
||||||
|
func b64url(b []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert small int (RSA exponent) to big-endian bytes
|
||||||
|
func fromInt(i int) []byte {
|
||||||
|
var x big.Int
|
||||||
|
x.SetInt64(int64(i))
|
||||||
|
return x.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- public accessors for JWKS ---
|
||||||
|
|
||||||
|
// KeyMeta is a minimal metadata view exposed for JWKS rendering.
|
||||||
|
type KeyMeta struct {
|
||||||
|
Alg string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetaFor returns minimal metadata (currently the alg) for a given kid.
|
||||||
|
// If not found, returns zero value (Alg == "").
|
||||||
|
func MetaFor(kid string) KeyMeta {
|
||||||
|
kc.mu.RLock()
|
||||||
|
defer kc.mu.RUnlock()
|
||||||
|
if m, ok := kc.meta[kid]; ok {
|
||||||
|
return KeyMeta{Alg: m.Alg}
|
||||||
|
}
|
||||||
|
return KeyMeta{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KcCopy invokes fn with a shallow copy of the public key map (kid -> public key instance).
|
||||||
|
// Useful to iterate without holding the lock during JSON building.
|
||||||
|
func KcCopy(fn func(map[string]interface{})) {
|
||||||
|
kc.mu.RLock()
|
||||||
|
defer kc.mu.RUnlock()
|
||||||
|
out := make(map[string]interface{}, len(kc.pub))
|
||||||
|
for kid, pk := range kc.pub {
|
||||||
|
out[kid] = pk
|
||||||
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
fn(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubToJWK converts a parsed public key into bare JWK parameters + kty.
|
||||||
|
// - RSA: returns n/e (base64url) and kty="RSA"
|
||||||
|
// - Ed25519: returns x (base64url) and kty="OKP"
|
||||||
|
func PubToJWK(_kid, _alg string, pub any) (map[string]string, string) {
|
||||||
|
switch k := pub.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return map[string]string{
|
||||||
|
"n": b64url(k.N.Bytes()),
|
||||||
|
"e": b64url(fromInt(k.E)),
|
||||||
|
}, "RSA"
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
return map[string]string{
|
||||||
|
"x": b64url([]byte(k)),
|
||||||
|
}, "OKP"
|
||||||
|
default:
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
55
internal/auth/jwt_issue.go
Normal file
55
internal/auth/jwt_issue.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueOpts struct {
|
||||||
|
Subject string
|
||||||
|
Issuer string
|
||||||
|
Audience string
|
||||||
|
TTL time.Duration
|
||||||
|
Claims map[string]any // extra app claims
|
||||||
|
}
|
||||||
|
|
||||||
|
func IssueAccessToken(opts IssueOpts) (string, error) {
|
||||||
|
kc.mu.RLock()
|
||||||
|
defer kc.mu.RUnlock()
|
||||||
|
|
||||||
|
if kc.selPriv == nil || kc.selKid == "" || kc.selAlg == "" {
|
||||||
|
return "", errors.New("no active signing key")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"iss": opts.Issuer,
|
||||||
|
"aud": opts.Audience,
|
||||||
|
"sub": opts.Subject,
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Add(opts.TTL).Unix(),
|
||||||
|
}
|
||||||
|
for k, v := range opts.Claims {
|
||||||
|
claims[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
var method jwt.SigningMethod
|
||||||
|
switch kc.selAlg {
|
||||||
|
case "RS256":
|
||||||
|
method = jwt.SigningMethodRS256
|
||||||
|
case "RS384":
|
||||||
|
method = jwt.SigningMethodRS384
|
||||||
|
case "RS512":
|
||||||
|
method = jwt.SigningMethodRS512
|
||||||
|
case "EdDSA":
|
||||||
|
method = jwt.SigningMethodEdDSA
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported alg")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(method, claims)
|
||||||
|
token.Header["kid"] = kc.selKid
|
||||||
|
|
||||||
|
return token.SignedString(kc.selPriv)
|
||||||
|
}
|
||||||
138
internal/auth/jwt_signer.go
Normal file
138
internal/auth/jwt_signer.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/keys"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
pub map[string]interface{} // kid -> public key object
|
||||||
|
meta map[string]models.SigningKey
|
||||||
|
selKid string
|
||||||
|
selAlg string
|
||||||
|
selPriv any
|
||||||
|
}
|
||||||
|
|
||||||
|
var kc keyCache
|
||||||
|
|
||||||
|
// Refresh loads active keys into memory. Call on startup and periodically (ticker/cron).
|
||||||
|
func Refresh(db *gorm.DB, encKeyB64 string) error {
|
||||||
|
var rows []models.SigningKey
|
||||||
|
if err := db.Where("is_active = true AND (expires_at IS NULL OR expires_at > ?)", time.Now()).
|
||||||
|
Order("created_at desc").Find(&rows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := make(map[string]interface{}, len(rows))
|
||||||
|
meta := make(map[string]models.SigningKey, len(rows))
|
||||||
|
var selKid string
|
||||||
|
var selAlg string
|
||||||
|
var selPriv any
|
||||||
|
|
||||||
|
for i, r := range rows {
|
||||||
|
// parse public
|
||||||
|
block, _ := pem.Decode([]byte(r.PublicPEM))
|
||||||
|
if block == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var pubKey any
|
||||||
|
switch r.Alg {
|
||||||
|
case "RS256", "RS384", "RS512":
|
||||||
|
pubKey, _ = x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
|
if pubKey == nil {
|
||||||
|
// also allow PKIX format
|
||||||
|
if k, err := x509.ParsePKIXPublicKey(block.Bytes); err == nil {
|
||||||
|
pubKey = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "EdDSA":
|
||||||
|
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
if edk, ok := k.(ed25519.PublicKey); ok {
|
||||||
|
pubKey = edk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pubKey == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pub[r.Kid] = pubKey
|
||||||
|
meta[r.Kid] = r
|
||||||
|
|
||||||
|
// pick first row as current signer (most recent because of order desc)
|
||||||
|
if i == 0 {
|
||||||
|
privPEM := r.PrivatePEM
|
||||||
|
// decrypt if necessary
|
||||||
|
if len(privPEM) > 10 && privPEM[:10] == "enc:aesgcm" {
|
||||||
|
pt, err := keysDecrypt(encKeyB64, privPEM)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
privPEM = string(pt)
|
||||||
|
}
|
||||||
|
blockPriv, _ := pem.Decode([]byte(privPEM))
|
||||||
|
if blockPriv == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch r.Alg {
|
||||||
|
case "RS256", "RS384", "RS512":
|
||||||
|
if k, err := x509.ParsePKCS1PrivateKey(blockPriv.Bytes); err == nil {
|
||||||
|
selPriv = k
|
||||||
|
selAlg = r.Alg
|
||||||
|
selKid = r.Kid
|
||||||
|
} else if kAny, err := x509.ParsePKCS8PrivateKey(blockPriv.Bytes); err == nil {
|
||||||
|
if k, ok := kAny.(*rsa.PrivateKey); ok {
|
||||||
|
selPriv = k
|
||||||
|
selAlg = r.Alg
|
||||||
|
selKid = r.Kid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "EdDSA":
|
||||||
|
if kAny, err := x509.ParsePKCS8PrivateKey(blockPriv.Bytes); err == nil {
|
||||||
|
if k, ok := kAny.(ed25519.PrivateKey); ok {
|
||||||
|
selPriv = k
|
||||||
|
selAlg = r.Alg
|
||||||
|
selKid = r.Kid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kc.mu.Lock()
|
||||||
|
defer kc.mu.Unlock()
|
||||||
|
kc.pub = pub
|
||||||
|
kc.meta = meta
|
||||||
|
kc.selKid = selKid
|
||||||
|
kc.selAlg = selAlg
|
||||||
|
kc.selPriv = selPriv
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func keysDecrypt(encKey, enc string) ([]byte, error) {
|
||||||
|
return keysDecryptImpl(encKey, enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// indirection for same package
|
||||||
|
var keysDecryptImpl = func(encKey, enc string) ([]byte, error) {
|
||||||
|
return nil, errors.New("not wired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up from keys package
|
||||||
|
func init() {
|
||||||
|
keysDecryptImpl = keysDecryptShim
|
||||||
|
}
|
||||||
|
|
||||||
|
func keysDecryptShim(encKey, enc string) ([]byte, error) {
|
||||||
|
return keys.Decrypt(encKey, enc)
|
||||||
|
}
|
||||||
56
internal/auth/jwt_validate.go
Normal file
56
internal/auth/jwt_validate.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateJWT verifies RS256/RS384/RS512/EdDSA tokens using the in-memory key cache.
|
||||||
|
// It honors kid when present, and falls back to any active key.
|
||||||
|
func ValidateJWT(tokenStr string, db *gorm.DB) *models.User {
|
||||||
|
cfg, _ := config.Load()
|
||||||
|
|
||||||
|
parser := jwt.NewParser(
|
||||||
|
jwt.WithIssuer(cfg.JWTIssuer),
|
||||||
|
jwt.WithAudience(cfg.JWTAudience),
|
||||||
|
jwt.WithValidMethods([]string{"RS256", "RS384", "RS512", "EdDSA"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
token, err := parser.Parse(tokenStr, func(t *jwt.Token) (any, error) {
|
||||||
|
// Resolve by kid first
|
||||||
|
kid, _ := t.Header["kid"].(string)
|
||||||
|
|
||||||
|
kc.mu.RLock()
|
||||||
|
defer kc.mu.RUnlock()
|
||||||
|
|
||||||
|
if kid != "" {
|
||||||
|
if k, ok := kc.pub[kid]; ok {
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: try first active key
|
||||||
|
for _, k := range kc.pub {
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
return nil, jwt.ErrTokenUnverifiable
|
||||||
|
})
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, _ := token.Claims.(jwt.MapClaims)
|
||||||
|
sub, _ := claims["sub"].(string)
|
||||||
|
uid, err := uuid.Parse(sub)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ? AND is_disabled = false", uid).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &u
|
||||||
|
}
|
||||||
105
internal/auth/refresh.go
Normal file
105
internal/auth/refresh.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// random opaque token (returned to client once)
|
||||||
|
func generateOpaqueToken(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshPair struct {
|
||||||
|
Plain string
|
||||||
|
Record models.RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue a new refresh token (new family if familyID == nil)
|
||||||
|
func IssueRefreshToken(db *gorm.DB, userID uuid.UUID, ttl time.Duration, familyID *uuid.UUID) (RefreshPair, error) {
|
||||||
|
plain, err := generateOpaqueToken(32)
|
||||||
|
if err != nil {
|
||||||
|
return RefreshPair{}, err
|
||||||
|
}
|
||||||
|
hash, err := HashSecretArgon2id(plain)
|
||||||
|
if err != nil {
|
||||||
|
return RefreshPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fid := uuid.New()
|
||||||
|
if familyID != nil {
|
||||||
|
fid = *familyID
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := models.RefreshToken{
|
||||||
|
UserID: userID,
|
||||||
|
FamilyID: fid,
|
||||||
|
TokenHash: hash,
|
||||||
|
ExpiresAt: time.Now().Add(ttl),
|
||||||
|
}
|
||||||
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
return RefreshPair{}, err
|
||||||
|
}
|
||||||
|
return RefreshPair{Plain: plain, Record: rec}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRefreshToken refresh token; returns record if valid & not revoked/expired
|
||||||
|
func ValidateRefreshToken(db *gorm.DB, plain string) (*models.RefreshToken, error) {
|
||||||
|
if plain == "" {
|
||||||
|
return nil, errors.New("empty")
|
||||||
|
}
|
||||||
|
// var rec models.RefreshToken
|
||||||
|
// We can't query by hash w/ Argon; scan candidates by expiry window. Keep small TTL (e.g. 30d).
|
||||||
|
if err := db.Where("expires_at > ? AND revoked_at IS NULL", time.Now()).
|
||||||
|
Find(&[]models.RefreshToken{}).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Better: add a prefix column to narrow scan; omitted for brevity.
|
||||||
|
|
||||||
|
// Pragmatic approach: single SELECT per token:
|
||||||
|
// Add a TokenHashSHA256 column for deterministic lookup if you want O(1). (Optional)
|
||||||
|
|
||||||
|
// Minimal: iterate limited set; for simplicity we fetch by created window:
|
||||||
|
var recs []models.RefreshToken
|
||||||
|
if err := db.Where("expires_at > ? AND revoked_at IS NULL", time.Now()).
|
||||||
|
Order("created_at desc").Limit(500).Find(&recs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, r := range recs {
|
||||||
|
ok, _ := VerifySecretArgon2id(r.TokenHash, plain)
|
||||||
|
if ok {
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeFamily revokes all tokens in a family (logout everywhere)
|
||||||
|
func RevokeFamily(db *gorm.DB, familyID uuid.UUID) error {
|
||||||
|
now := time.Now()
|
||||||
|
return db.Model(&models.RefreshToken{}).
|
||||||
|
Where("family_id = ? AND revoked_at IS NULL", familyID).
|
||||||
|
Update("revoked_at", &now).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateRefreshToken replaces one token with a fresh one within the same family
|
||||||
|
func RotateRefreshToken(db *gorm.DB, used *models.RefreshToken, ttl time.Duration) (RefreshPair, error) {
|
||||||
|
// revoke the used token (one-time use)
|
||||||
|
now := time.Now()
|
||||||
|
if err := db.Model(&models.RefreshToken{}).
|
||||||
|
Where("id = ? AND revoked_at IS NULL", used.ID).
|
||||||
|
Update("revoked_at", &now).Error; err != nil {
|
||||||
|
return RefreshPair{}, err
|
||||||
|
}
|
||||||
|
return IssueRefreshToken(db, used.UserID, ttl, &used.FamilyID)
|
||||||
|
}
|
||||||
88
internal/auth/validate_keys.go
Normal file
88
internal/auth/validate_keys.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateAPIKey validates a single-token user API key sent via X-API-KEY.
|
||||||
|
func ValidateAPIKey(rawKey string, db *gorm.DB) *models.User {
|
||||||
|
if rawKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
digest := SHA256Hex(rawKey)
|
||||||
|
|
||||||
|
var k models.APIKey
|
||||||
|
if err := db.
|
||||||
|
Where("key_hash = ? AND scope = ? AND (expires_at IS NULL OR expires_at > ?)", digest, "user", time.Now()).
|
||||||
|
First(&k).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if k.UserID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ? AND is_disabled = false", *k.UserID).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Optional: touch last_used_at here if you've added it on the model.
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAppKeyPair validates a user key/secret pair via X-APP-KEY / X-APP-SECRET.
|
||||||
|
func ValidateAppKeyPair(appKey, secret string, db *gorm.DB) *models.User {
|
||||||
|
if appKey == "" || secret == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
digest := SHA256Hex(appKey)
|
||||||
|
|
||||||
|
var k models.APIKey
|
||||||
|
if err := db.
|
||||||
|
Where("key_hash = ? AND scope = ? AND (expires_at IS NULL OR expires_at > ?)", digest, "user", time.Now()).
|
||||||
|
First(&k).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, _ := VerifySecretArgon2id(zeroIfNil(k.SecretHash), secret)
|
||||||
|
if !ok || k.UserID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ? AND is_disabled = false", *k.UserID).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateOrgKeyPair validates an org key/secret via X-ORG-KEY / X-ORG-SECRET.
|
||||||
|
func ValidateOrgKeyPair(orgKey, secret string, db *gorm.DB) *models.Organization {
|
||||||
|
if orgKey == "" || secret == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
digest := SHA256Hex(orgKey)
|
||||||
|
|
||||||
|
var k models.APIKey
|
||||||
|
if err := db.
|
||||||
|
Where("key_hash = ? AND scope = ? AND (expires_at IS NULL OR expires_at > ?)", digest, "org", time.Now()).
|
||||||
|
First(&k).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, _ := VerifySecretArgon2id(zeroIfNil(k.SecretHash), secret)
|
||||||
|
if !ok || k.OrgID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var o models.Organization
|
||||||
|
if err := db.First(&o, "id = ?", *k.OrgID).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
// local helper; avoids nil-deref when comparing secrets
|
||||||
|
func zeroIfNil(s *string) string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *s
|
||||||
|
}
|
||||||
194
internal/config/config.go
Normal file
194
internal/config/config.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DbURL string
|
||||||
|
Port string
|
||||||
|
Host string
|
||||||
|
JWTIssuer string
|
||||||
|
JWTAudience string
|
||||||
|
JWTPrivateEncKey string
|
||||||
|
OAuthRedirectBase string
|
||||||
|
GoogleClientID string
|
||||||
|
GoogleClientSecret string
|
||||||
|
GithubClientID string
|
||||||
|
GithubClientSecret string
|
||||||
|
|
||||||
|
UIDev bool
|
||||||
|
Env string
|
||||||
|
Debug bool
|
||||||
|
Swagger bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
cached Config
|
||||||
|
loadErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load() (Config, error) {
|
||||||
|
once.Do(func() {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
|
// Use a private viper to avoid global mutation/races
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
v.SetDefault("bind.address", "127.0.0.1")
|
||||||
|
v.SetDefault("bind.port", "8080")
|
||||||
|
v.SetDefault("database.url", "postgres://user:pass@localhost:5432/db?sslmode=disable")
|
||||||
|
|
||||||
|
v.SetDefault("ui.dev", false)
|
||||||
|
v.SetDefault("env", "development")
|
||||||
|
v.SetDefault("debug", false)
|
||||||
|
v.SetDefault("swagger", false)
|
||||||
|
|
||||||
|
// Env setup and binding
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
v.AutomaticEnv()
|
||||||
|
|
||||||
|
keys := []string{
|
||||||
|
"bind.address",
|
||||||
|
"bind.port",
|
||||||
|
"database.url",
|
||||||
|
"jwt.issuer",
|
||||||
|
"jwt.audience",
|
||||||
|
"jwt.private.enc.key",
|
||||||
|
"oauth.redirect.base",
|
||||||
|
"google.client.id",
|
||||||
|
"google.client.secret",
|
||||||
|
"github.client.id",
|
||||||
|
"github.client.secret",
|
||||||
|
"ui.dev",
|
||||||
|
"env",
|
||||||
|
"debug",
|
||||||
|
"swagger",
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
_ = v.BindEnv(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config
|
||||||
|
cfg := Config{
|
||||||
|
DbURL: v.GetString("database.url"),
|
||||||
|
Port: v.GetString("bind.port"),
|
||||||
|
Host: v.GetString("bind.address"),
|
||||||
|
JWTIssuer: v.GetString("jwt.issuer"),
|
||||||
|
JWTAudience: v.GetString("jwt.audience"),
|
||||||
|
JWTPrivateEncKey: v.GetString("jwt.private.enc.key"),
|
||||||
|
OAuthRedirectBase: v.GetString("oauth.redirect.base"),
|
||||||
|
GoogleClientID: v.GetString("google.client.id"),
|
||||||
|
GoogleClientSecret: v.GetString("google.client.secret"),
|
||||||
|
GithubClientID: v.GetString("github.client.id"),
|
||||||
|
GithubClientSecret: v.GetString("github.client.secret"),
|
||||||
|
|
||||||
|
UIDev: v.GetBool("ui.dev"),
|
||||||
|
Env: v.GetString("env"),
|
||||||
|
Debug: v.GetBool("debug"),
|
||||||
|
Swagger: v.GetBool("swagger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if err := validateConfig(cfg); err != nil {
|
||||||
|
loadErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cached = cfg
|
||||||
|
})
|
||||||
|
return cached, loadErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(cfg Config) error {
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
// Required general settings
|
||||||
|
req := map[string]string{
|
||||||
|
"jwt.issuer": cfg.JWTIssuer,
|
||||||
|
"jwt.audience": cfg.JWTAudience,
|
||||||
|
"jwt.private.enc.key": cfg.JWTPrivateEncKey,
|
||||||
|
"oauth.redirect.base": cfg.OAuthRedirectBase,
|
||||||
|
}
|
||||||
|
for k, v := range req {
|
||||||
|
if strings.TrimSpace(v) == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("missing required config key %q (env %s)", k, envNameFromKey(k)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth provider requirements:
|
||||||
|
googleOK := strings.TrimSpace(cfg.GoogleClientID) != "" && strings.TrimSpace(cfg.GoogleClientSecret) != ""
|
||||||
|
githubOK := strings.TrimSpace(cfg.GithubClientID) != "" && strings.TrimSpace(cfg.GithubClientSecret) != ""
|
||||||
|
|
||||||
|
// If partially configured, report what's missing for each
|
||||||
|
if !googleOK && (cfg.GoogleClientID != "" || cfg.GoogleClientSecret != "") {
|
||||||
|
if cfg.GoogleClientID == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("google.client.id is missing (env %s) while google.client.secret is set", envNameFromKey("google.client.id")))
|
||||||
|
}
|
||||||
|
if cfg.GoogleClientSecret == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("google.client.secret is missing (env %s) while google.client.id is set", envNameFromKey("google.client.secret")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !githubOK && (cfg.GithubClientID != "" || cfg.GithubClientSecret != "") {
|
||||||
|
if cfg.GithubClientID == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("github.client.id is missing (env %s) while github.client.secret is set", envNameFromKey("github.client.id")))
|
||||||
|
}
|
||||||
|
if cfg.GithubClientSecret == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("github.client.secret is missing (env %s) while github.client.id is set", envNameFromKey("github.client.secret")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce minimum: at least one full provider
|
||||||
|
if !googleOK && !githubOK {
|
||||||
|
errs = append(errs, "at least one OAuth provider must be fully configured: either Google (google.client.id + google.client.secret) or GitHub (github.client.id + github.client.secret)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errors.New(strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envNameFromKey(key string) string {
|
||||||
|
return strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugPrintConfig() {
|
||||||
|
cfg, _ := Load()
|
||||||
|
b, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error marshalling config:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Loaded configuration:")
|
||||||
|
fmt.Println(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUIDev() bool {
|
||||||
|
cfg, _ := Load()
|
||||||
|
return cfg.UIDev
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDev() bool {
|
||||||
|
cfg, _ := Load()
|
||||||
|
return strings.EqualFold(cfg.Env, "development")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDebug() bool {
|
||||||
|
cfg, _ := Load()
|
||||||
|
return cfg.Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSwaggerEnabled() bool {
|
||||||
|
cfg, _ := Load()
|
||||||
|
return cfg.Swagger
|
||||||
|
}
|
||||||
17
internal/db/db.go
Normal file
17
internal/db/db.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Open(dsn string) *gorm.DB {
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Warn)})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to db: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
25
internal/db/migrate.go
Normal file
25
internal/db/migrate.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(db *gorm.DB, models ...any) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 0) Extensions
|
||||||
|
if err := tx.Exec(`CREATE EXTENSION IF NOT EXISTS pgcrypto`).Error; err != nil {
|
||||||
|
return fmt.Errorf("enable pgcrypto: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Exec(`CREATE EXTENSION IF NOT EXISTS citext`).Error; err != nil {
|
||||||
|
return fmt.Errorf("enable citext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) AutoMigrate (pass parents before children in caller)
|
||||||
|
if err := tx.AutoMigrate(models...); err != nil {
|
||||||
|
return fmt.Errorf("automigrate: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
477
internal/handlers/auth.go
Normal file
477
internal/handlers/auth.go
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/config"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthProvider struct {
|
||||||
|
Name string
|
||||||
|
Issuer string
|
||||||
|
Scopes []string
|
||||||
|
ClientID string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerConfig(cfg config.Config, name string) (oauthProvider, bool) {
|
||||||
|
switch strings.ToLower(name) {
|
||||||
|
case "google":
|
||||||
|
return oauthProvider{
|
||||||
|
Name: "google",
|
||||||
|
Issuer: "https://accounts.google.com",
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "email", "profile"},
|
||||||
|
ClientID: cfg.GoogleClientID,
|
||||||
|
Secret: cfg.GoogleClientSecret,
|
||||||
|
}, true
|
||||||
|
case "github":
|
||||||
|
// GitHub is not a pure OIDC provider; we use OAuth2 + user email API
|
||||||
|
return oauthProvider{
|
||||||
|
Name: "github",
|
||||||
|
Issuer: "github",
|
||||||
|
Scopes: []string{"read:user", "user:email"},
|
||||||
|
ClientID: cfg.GithubClientID, Secret: cfg.GithubClientSecret,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
return oauthProvider{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthStart godoc
|
||||||
|
// @ID AuthStart
|
||||||
|
// @Summary Begin social login
|
||||||
|
// @Description Returns provider authorization URL for the frontend to redirect
|
||||||
|
// @Tags Auth
|
||||||
|
// @Param provider path string true "google|github"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.AuthStartResponse
|
||||||
|
// @Router /auth/{provider}/start [post]
|
||||||
|
func AuthStart(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, _ := config.Load()
|
||||||
|
provider := strings.ToLower(chi.URLParam(r, "provider"))
|
||||||
|
|
||||||
|
p, ok := providerConfig(cfg, provider)
|
||||||
|
if !ok || p.ClientID == "" || p.Secret == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "unsupported_provider", "provider not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect := cfg.OAuthRedirectBase + "/api/v1/auth/" + p.Name + "/callback"
|
||||||
|
|
||||||
|
// Optional SPA hints to be embedded into state
|
||||||
|
mode := r.URL.Query().Get("mode") // "spa" enables postMessage callback page
|
||||||
|
origin := r.URL.Query().Get("origin") // e.g. http://localhost:5173
|
||||||
|
|
||||||
|
state := uuid.NewString()
|
||||||
|
if mode == "spa" && origin != "" {
|
||||||
|
state = state + "|mode=spa|origin=" + url.QueryEscape(origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authURL string
|
||||||
|
|
||||||
|
if p.Issuer == "github" {
|
||||||
|
o := &oauth2.Config{
|
||||||
|
ClientID: p.ClientID,
|
||||||
|
ClientSecret: p.Secret,
|
||||||
|
RedirectURL: redirect,
|
||||||
|
Scopes: p.Scopes,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://github.com/login/oauth/authorize",
|
||||||
|
TokenURL: "https://github.com/login/oauth/access_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
authURL = o.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||||
|
} else {
|
||||||
|
// Google OIDC
|
||||||
|
ctx := context.Background()
|
||||||
|
prov, err := oidc.NewProvider(ctx, p.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "oidc_discovery_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o := &oauth2.Config{
|
||||||
|
ClientID: p.ClientID,
|
||||||
|
ClientSecret: p.Secret,
|
||||||
|
RedirectURL: redirect,
|
||||||
|
Endpoint: prov.Endpoint(),
|
||||||
|
Scopes: p.Scopes,
|
||||||
|
}
|
||||||
|
authURL = o.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, dto.AuthStartResponse{AuthURL: authURL})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthCallback godoc
|
||||||
|
// @ID AuthCallback
|
||||||
|
// @Summary Handle social login callback
|
||||||
|
// @Tags Auth
|
||||||
|
// @Param provider path string true "google|github"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.TokenPair
|
||||||
|
// @Router /auth/{provider}/callback [get]
|
||||||
|
func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, _ := config.Load()
|
||||||
|
provider := strings.ToLower(chi.URLParam(r, "provider"))
|
||||||
|
|
||||||
|
p, ok := providerConfig(cfg, provider)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "unsupported_provider", "provider not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
if code == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_request", "missing code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redirect := cfg.OAuthRedirectBase + "/api/v1/auth/" + p.Name + "/callback"
|
||||||
|
|
||||||
|
var email, display, subject string
|
||||||
|
|
||||||
|
if p.Issuer == "github" {
|
||||||
|
// OAuth2 code exchange
|
||||||
|
o := &oauth2.Config{
|
||||||
|
ClientID: p.ClientID,
|
||||||
|
ClientSecret: p.Secret,
|
||||||
|
RedirectURL: redirect,
|
||||||
|
Scopes: p.Scopes,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://github.com/login/oauth/authorize",
|
||||||
|
TokenURL: "https://github.com/login/oauth/access_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tok, err := o.Exchange(r.Context(), code)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "exchange_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Fetch user primary email
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
|
||||||
|
req.Header.Set("Authorization", "token "+tok.AccessToken)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "email_fetch_failed", "github user/emails")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var emails []struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&emails); err != nil || len(emails) == 0 {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "email_parse_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email = emails[0].Email
|
||||||
|
for _, e := range emails {
|
||||||
|
if e.Primary {
|
||||||
|
email = e.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subject = "github:" + email
|
||||||
|
display = strings.Split(email, "@")[0]
|
||||||
|
} else {
|
||||||
|
// Google OIDC
|
||||||
|
oidcProv, err := oidc.NewProvider(r.Context(), p.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "oidc_discovery_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o := &oauth2.Config{
|
||||||
|
ClientID: p.ClientID,
|
||||||
|
ClientSecret: p.Secret,
|
||||||
|
RedirectURL: redirect,
|
||||||
|
Endpoint: oidcProv.Endpoint(),
|
||||||
|
Scopes: p.Scopes,
|
||||||
|
}
|
||||||
|
tok, err := o.Exchange(r.Context(), code)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 401, "exchange_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := oidcProv.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||||
|
rawIDToken, ok := tok.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "no_id_token", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idt, err := verifier.Verify(r.Context(), rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 401, "id_token_invalid", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
EmailVerified bool `json:"email_verified"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
}
|
||||||
|
if err := idt.Claims(&claims); err != nil {
|
||||||
|
utils.WriteError(w, 401, "claims_parse_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email = strings.ToLower(claims.Email)
|
||||||
|
display = claims.Name
|
||||||
|
subject = "google:" + claims.Sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert Account + User; domain auto-join (member)
|
||||||
|
user, err := upsertAccountAndUser(db, p.Name, subject, email, display)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "account_upsert_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Org auto-join: Organization.Domain == email domain
|
||||||
|
_ = ensureAutoMembership(db, user.ID, email)
|
||||||
|
|
||||||
|
// Issue tokens
|
||||||
|
accessTTL := 1 * time.Hour
|
||||||
|
refreshTTL := 30 * 24 * time.Hour
|
||||||
|
|
||||||
|
access, err := auth.IssueAccessToken(auth.IssueOpts{
|
||||||
|
Subject: user.ID.String(),
|
||||||
|
Issuer: cfg.JWTIssuer,
|
||||||
|
Audience: cfg.JWTAudience,
|
||||||
|
TTL: accessTTL,
|
||||||
|
Claims: map[string]any{
|
||||||
|
"email": email,
|
||||||
|
"name": display,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "issue_access_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rp, err := auth.IssueRefreshToken(db, user.ID, refreshTTL, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "issue_refresh_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
if strings.Contains(state, "mode=spa") {
|
||||||
|
origin := ""
|
||||||
|
for _, part := range strings.Split(state, "|") {
|
||||||
|
if strings.HasPrefix(part, "origin=") {
|
||||||
|
origin, _ = url.QueryUnescape(strings.TrimPrefix(part, "origin="))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: restrict to backend origin if none supplied
|
||||||
|
if origin == "" {
|
||||||
|
origin = cfg.OAuthRedirectBase
|
||||||
|
}
|
||||||
|
payload := dto.TokenPair{
|
||||||
|
AccessToken: access,
|
||||||
|
RefreshToken: rp.Plain,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: int64(accessTTL.Seconds()),
|
||||||
|
}
|
||||||
|
writePostMessageHTML(w, origin, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default JSON response
|
||||||
|
utils.WriteJSON(w, http.StatusOK, dto.TokenPair{
|
||||||
|
AccessToken: access,
|
||||||
|
RefreshToken: rp.Plain,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: int64(accessTTL.Seconds()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh godoc
|
||||||
|
// @ID Refresh
|
||||||
|
// @Summary Rotate refresh token
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body dto.RefreshRequest true "Refresh token"
|
||||||
|
// @Success 200 {object} dto.TokenPair
|
||||||
|
// @Router /auth/refresh [post]
|
||||||
|
func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, _ := config.Load()
|
||||||
|
var req dto.RefreshRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec, err := auth.ValidateRefreshToken(db, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 401, "invalid_refresh", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ? AND is_disabled = false", rec.UserID).Error; err != nil {
|
||||||
|
utils.WriteError(w, 401, "user_disabled", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate
|
||||||
|
newPair, err := auth.RotateRefreshToken(db, rec, 30*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "rotate_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// new access
|
||||||
|
access, err := auth.IssueAccessToken(auth.IssueOpts{
|
||||||
|
Subject: u.ID.String(),
|
||||||
|
Issuer: cfg.JWTIssuer,
|
||||||
|
Audience: cfg.JWTAudience,
|
||||||
|
TTL: 1 * time.Hour,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "issue_access_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, 200, dto.TokenPair{
|
||||||
|
AccessToken: access,
|
||||||
|
RefreshToken: newPair.Plain,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout godoc
|
||||||
|
// @ID Logout
|
||||||
|
// @Summary Revoke refresh token family (logout everywhere)
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body dto.LogoutRequest true "Refresh token"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Router /auth/logout [post]
|
||||||
|
func Logout(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req dto.LogoutRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec, err := auth.ValidateRefreshToken(db, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(204) // already invalid/revoked
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := auth.RevokeFamily(db, rec.FamilyID); err != nil {
|
||||||
|
utils.WriteError(w, 500, "revoke_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
func upsertAccountAndUser(db *gorm.DB, provider, subject, email, display string) (*models.User, error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
var acc models.Account
|
||||||
|
if err := db.Where("provider = ? AND subject = ?", provider, subject).First(&acc).Error; err == nil {
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ?", acc.UserID).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
// Link by email if exists
|
||||||
|
var ue models.UserEmail
|
||||||
|
if err := db.Where("LOWER(email) = ?", email).First(&ue).Error; err == nil {
|
||||||
|
acc = models.Account{
|
||||||
|
UserID: ue.UserID,
|
||||||
|
Provider: provider,
|
||||||
|
Subject: subject,
|
||||||
|
Email: &email,
|
||||||
|
EmailVerified: true,
|
||||||
|
}
|
||||||
|
if err := db.Create(&acc).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var u models.User
|
||||||
|
if err := db.First(&u, "id = ?", ue.UserID).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
// Create user
|
||||||
|
u := models.User{DisplayName: &display, PrimaryEmail: &email}
|
||||||
|
if err := db.Create(&u).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ue = models.UserEmail{UserID: u.ID, Email: email, IsVerified: true, IsPrimary: true}
|
||||||
|
_ = db.Create(&ue).Error
|
||||||
|
acc = models.Account{UserID: u.ID, Provider: provider, Subject: subject, Email: &email, EmailVerified: true}
|
||||||
|
_ = db.Create(&acc).Error
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAutoMembership(db *gorm.DB, userID uuid.UUID, email string) error {
|
||||||
|
parts := strings.SplitN(strings.ToLower(email), "@", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
domain := parts[1]
|
||||||
|
var org models.Organization
|
||||||
|
if err := db.Where("LOWER(domain) = ?", domain).First(&org).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// if already member, done
|
||||||
|
var c int64
|
||||||
|
db.Model(&models.Membership{}).
|
||||||
|
Where("user_id = ? AND organization_id = ?", userID, org.ID).
|
||||||
|
Count(&c)
|
||||||
|
if c > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.Create(&models.Membership{
|
||||||
|
UserID: userID, OrganizationID: org.ID, Role: "member",
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePostMessageHTML sends a tiny HTML page that posts tokens to the SPA and closes the window.
|
||||||
|
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
||||||
|
b, _ := json.Marshal(payload)
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`<!doctype html><html><body><script>
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
var data = ` + string(b) + `;
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage({ type: 'autoglue:auth', payload: data }, '` + origin + `');
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
window.close();
|
||||||
|
})();
|
||||||
|
</script></body></html>`))
|
||||||
|
}
|
||||||
24
internal/handlers/dto/auth.go
Normal file
24
internal/handlers/dto/auth.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// swagger:model AuthStartResponse
|
||||||
|
type AuthStartResponse struct {
|
||||||
|
AuthURL string `json:"auth_url" example:"https://accounts.google.com/o/oauth2/v2/auth?client_id=..."`
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model TokenPair
|
||||||
|
type TokenPair struct {
|
||||||
|
AccessToken string `json:"access_token" example:"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ij..."`
|
||||||
|
RefreshToken string `json:"refresh_token" example:"m0l9o8rT3t0V8d3eFf...."`
|
||||||
|
TokenType string `json:"token_type" example:"Bearer"`
|
||||||
|
ExpiresIn int64 `json:"expires_in" example:"3600"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model RefreshRequest
|
||||||
|
type RefreshRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" example:"m0l9o8rT3t0V8d3eFf..."`
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model LogoutRequest
|
||||||
|
type LogoutRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" example:"m0l9o8rT3t0V8d3eFf..."`
|
||||||
|
}
|
||||||
19
internal/handlers/dto/jwks.go
Normal file
19
internal/handlers/dto/jwks.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// JWK represents a single JSON Web Key (public only).
|
||||||
|
// swagger:model JWK
|
||||||
|
type JWK struct {
|
||||||
|
Kty string `json:"kty" example:"RSA" gorm:"-"`
|
||||||
|
Use string `json:"use,omitempty" example:"sig" gorm:"-"`
|
||||||
|
Kid string `json:"kid,omitempty" example:"7c6f1d0a-7a98-4e6a-9dbf-6b1af4b9f345" gorm:"-"`
|
||||||
|
Alg string `json:"alg,omitempty" example:"RS256" gorm:"-"`
|
||||||
|
N string `json:"n,omitempty" gorm:"-"`
|
||||||
|
E string `json:"e,omitempty" example:"AQAB" gorm:"-"`
|
||||||
|
X string `json:"x,omitempty" gorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKS is a JSON Web Key Set container.
|
||||||
|
// swagger:model JWKS
|
||||||
|
type JWKS struct {
|
||||||
|
Keys []JWK `json:"keys" gorm:"-"`
|
||||||
|
}
|
||||||
37
internal/handlers/dto/servers.go
Normal file
37
internal/handlers/dto/servers.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type CreateServerRequest struct {
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
PublicIPAddress string `json:"public_ip_address,omitempty"`
|
||||||
|
PrivateIPAddress string `json:"private_ip_address"`
|
||||||
|
SSHUser string `json:"ssh_user"`
|
||||||
|
SshKeyID string `json:"ssh_key_id"`
|
||||||
|
Role string `json:"role" example:"master|worker|bastion"`
|
||||||
|
Status string `json:"status,omitempty" example:"pending|provisioning|ready|failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateServerRequest struct {
|
||||||
|
Hostname *string `json:"hostname,omitempty"`
|
||||||
|
PublicIPAddress *string `json:"public_ip_address,omitempty"`
|
||||||
|
PrivateIPAddress *string `json:"private_ip_address,omitempty"`
|
||||||
|
SSHUser *string `json:"ssh_user,omitempty"`
|
||||||
|
SshKeyID *string `json:"ssh_key_id,omitempty"`
|
||||||
|
Role *string `json:"role,omitempty" example:"master|worker|bastion"`
|
||||||
|
Status *string `json:"status,omitempty" example:"pending|provisioning|ready|failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
PublicIPAddress *string `json:"public_ip_address,omitempty"`
|
||||||
|
PrivateIPAddress string `json:"private_ip_address"`
|
||||||
|
SSHUser string `json:"ssh_user"`
|
||||||
|
SshKeyID uuid.UUID `json:"ssh_key_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt string `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
38
internal/handlers/dto/ssh.go
Normal file
38
internal/handlers/dto/ssh.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type CreateSSHRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Comment string `json:"comment,omitempty" example:"deploy@autoglue"`
|
||||||
|
Bits *int `json:"bits,omitempty"` // Only for RSA
|
||||||
|
Type *string `json:"type,omitempty"` // "rsa" (default) or "ed25519"
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt string `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshRevealResponse struct {
|
||||||
|
SshResponse
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshMaterialJSON struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
// Exactly one of the following will be populated for part=public/private.
|
||||||
|
PublicKey *string `json:"public_key,omitempty"` // OpenSSH authorized_key (string)
|
||||||
|
PrivatePEM *string `json:"private_pem,omitempty"` // PKCS#1/PEM (string)
|
||||||
|
// For part=both with mode=json we'll return a base64 zip
|
||||||
|
ZipBase64 *string `json:"zip_base64,omitempty"` // base64-encoded zip
|
||||||
|
// Suggested filenames (SDKs can save to disk without inferring names)
|
||||||
|
Filenames []string `json:"filenames"`
|
||||||
|
}
|
||||||
22
internal/handlers/dto/taints.go
Normal file
22
internal/handlers/dto/taints.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type TaintResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaintRequest struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTaintRequest struct {
|
||||||
|
Key *string `json:"key,omitempty"`
|
||||||
|
Value *string `json:"value,omitempty"`
|
||||||
|
Effect *string `json:"effect,omitempty"`
|
||||||
|
}
|
||||||
56
internal/handlers/jwks.go
Normal file
56
internal/handlers/jwks.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jwk struct {
|
||||||
|
Kty string `json:"kty"`
|
||||||
|
Use string `json:"use,omitempty"`
|
||||||
|
Kid string `json:"kid,omitempty"`
|
||||||
|
Alg string `json:"alg,omitempty"`
|
||||||
|
N string `json:"n,omitempty"` // RSA modulus (base64url)
|
||||||
|
E string `json:"e,omitempty"` // RSA exponent (base64url)
|
||||||
|
X string `json:"x,omitempty"` // Ed25519 public key (base64url)
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwks struct {
|
||||||
|
Keys []jwk `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKSHandler godoc
|
||||||
|
// @ID getJWKS
|
||||||
|
// @Summary Get JWKS
|
||||||
|
// @Description Returns the JSON Web Key Set for token verification
|
||||||
|
// @Tags Auth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.JWKS
|
||||||
|
// @Router /.well-known/jwks.json [get]
|
||||||
|
func JWKSHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
out := dto.JWKS{Keys: make([]dto.JWK, 0)}
|
||||||
|
|
||||||
|
auth.KcCopy(func(pub map[string]interface{}) {
|
||||||
|
for kid, pk := range pub {
|
||||||
|
meta := auth.MetaFor(kid)
|
||||||
|
params, kty := auth.PubToJWK(kid, meta.Alg, pk)
|
||||||
|
if kty == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := dto.JWK{
|
||||||
|
Kty: kty,
|
||||||
|
Use: "sig",
|
||||||
|
Kid: kid,
|
||||||
|
Alg: meta.Alg,
|
||||||
|
N: params["n"],
|
||||||
|
E: params["e"],
|
||||||
|
X: params["x"],
|
||||||
|
}
|
||||||
|
out.Keys = append(out.Keys, j)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
120
internal/handlers/me.go
Normal file
120
internal/handlers/me.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type meResponse struct {
|
||||||
|
models.User `json:",inline"`
|
||||||
|
Emails []models.UserEmail `json:"emails"`
|
||||||
|
Organizations []models.Organization `json:"organizations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMe godoc
|
||||||
|
// @ID GetMe
|
||||||
|
// @Summary Get current user profile
|
||||||
|
// @Tags Me
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} meResponse
|
||||||
|
// @Router /me [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func GetMe(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if err := db.First(&user, "id = ? AND is_disabled = false", u.ID).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "user not found/disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var emails []models.UserEmail
|
||||||
|
_ = db.Where("user_id = ?", user.ID).Order("is_primary desc, created_at asc").Find(&emails).Error
|
||||||
|
|
||||||
|
var orgs []models.Organization
|
||||||
|
{
|
||||||
|
var rows []models.Membership
|
||||||
|
_ = db.Where("user_id = ?", user.ID).Find(&rows).Error
|
||||||
|
if len(rows) > 0 {
|
||||||
|
var ids []interface{}
|
||||||
|
for _, m := range rows {
|
||||||
|
ids = append(ids, m.OrganizationID)
|
||||||
|
}
|
||||||
|
_ = db.Find(&orgs, "id IN ?", ids).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, meResponse{
|
||||||
|
User: user,
|
||||||
|
Emails: emails,
|
||||||
|
Organizations: orgs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateMeRequest struct {
|
||||||
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
|
// You can add more editable fields here (timezone, avatar, etc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMe godoc
|
||||||
|
// @ID UpdateMe
|
||||||
|
// @Summary Update current user profile
|
||||||
|
// @Tags Me
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body updateMeRequest true "Patch profile"
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Router /me [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func UpdateMe(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateMeRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_json", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
|
||||||
|
if req.DisplayName != nil {
|
||||||
|
updates["display_name"] = req.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) == 0 {
|
||||||
|
var user models.User
|
||||||
|
if err := db.First(&user, "id = ?", u.ID).Error; err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, 200, user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.User{}).Where("id = ?", u.ID).Updates(updates).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var out models.User
|
||||||
|
_ = db.First(&out, "id = ?", u.ID).Error
|
||||||
|
utils.WriteJSON(w, 200, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
175
internal/handlers/me_keys.go
Normal file
175
internal/handlers/me_keys.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userAPIKeyOut struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Scope string `json:"scope"` // "user"
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||||
|
Plain *string `json:"plain,omitempty"` // Shown only on create:
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserAPIKeys godoc
|
||||||
|
// @ID ListUserAPIKeys
|
||||||
|
// @Summary List my API keys
|
||||||
|
// @Tags Me / API Keys
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} userAPIKeyOut
|
||||||
|
// @Router /me/api-keys [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func ListUserAPIKeys(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rows []models.APIKey
|
||||||
|
if err := db.
|
||||||
|
Where("scope = ? AND user_id = ?", "user", u.ID).
|
||||||
|
Order("created_at desc").
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]userAPIKeyOut, 0, len(rows))
|
||||||
|
for _, k := range rows {
|
||||||
|
out = append(out, toUserKeyOut(k, nil))
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, 200, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type createUserKeyRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ExpiresInHours *int `json:"expires_in_hours,omitempty"` // optional TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserAPIKey godoc
|
||||||
|
// @ID CreateUserAPIKey
|
||||||
|
// @Summary Create a new user API key
|
||||||
|
// @Description Returns the plaintext key once. Store it securely on the client side.
|
||||||
|
// @Tags Me / API Keys
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body createUserKeyRequest true "Key options"
|
||||||
|
// @Success 201 {object} userAPIKeyOut
|
||||||
|
// @Router /me/api-keys [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func CreateUserAPIKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req createUserKeyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plain, err := generateUserAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "gen_failed", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash := auth.SHA256Hex(plain)
|
||||||
|
|
||||||
|
var exp *time.Time
|
||||||
|
if req.ExpiresInHours != nil && *req.ExpiresInHours > 0 {
|
||||||
|
t := time.Now().Add(time.Duration(*req.ExpiresInHours) * time.Hour)
|
||||||
|
exp = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := models.APIKey{
|
||||||
|
Scope: "user",
|
||||||
|
UserID: &u.ID,
|
||||||
|
KeyHash: hash,
|
||||||
|
Name: req.Name, // if field exists
|
||||||
|
ExpiresAt: exp,
|
||||||
|
// SecretHash: nil (not used for user keys)
|
||||||
|
}
|
||||||
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, toUserKeyOut(rec, &plain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserAPIKey godoc
|
||||||
|
// @ID DeleteUserAPIKey
|
||||||
|
// @Summary Delete a user API key
|
||||||
|
// @Tags Me / API Keys
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Key ID (UUID)"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Router /me/api-keys/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
func DeleteUserAPIKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_id", "must be uuid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx := db.Where("id = ? AND scope = ? AND user_id = ?", id, "user", u.ID).
|
||||||
|
Delete(&models.APIKey{})
|
||||||
|
if tx.Error != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", tx.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, 404, "not_found", "key not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUserKeyOut(k models.APIKey, plain *string) userAPIKeyOut {
|
||||||
|
return userAPIKeyOut{
|
||||||
|
ID: k.ID,
|
||||||
|
Name: &k.Name, // if your model has it; else remove
|
||||||
|
Scope: k.Scope,
|
||||||
|
CreatedAt: k.CreatedAt,
|
||||||
|
ExpiresAt: k.ExpiresAt,
|
||||||
|
LastUsedAt: k.LastUsedAt, // if present; else remove
|
||||||
|
Plain: plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUserAPIKey() (string, error) {
|
||||||
|
// 24 random bytes → base64url (no padding), with "u_" prefix
|
||||||
|
b := make([]byte, 24)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s := base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
return "u_" + s, nil
|
||||||
|
}
|
||||||
647
internal/handlers/orgs.go
Normal file
647
internal/handlers/orgs.go
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/auth"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- Helpers ----------
|
||||||
|
|
||||||
|
func mustUser(r *http.Request) (*models.User, bool) {
|
||||||
|
return httpmiddleware.UserFrom(r.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOrgRole(db *gorm.DB, userID, orgID uuid.UUID, want ...string) (bool, string) {
|
||||||
|
var m models.Membership
|
||||||
|
if err := db.Where("user_id = ? AND organization_id = ?", userID, orgID).First(&m).Error; err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
got := strings.ToLower(m.Role)
|
||||||
|
for _, w := range want {
|
||||||
|
if got == strings.ToLower(w) {
|
||||||
|
return true, got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, got
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMember(db *gorm.DB, userID, orgID uuid.UUID) bool {
|
||||||
|
ok, _ := isOrgRole(db, userID, orgID, "owner", "admin", "member")
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomB64URL(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Orgs: list/create/get/update/delete ----------
|
||||||
|
|
||||||
|
type orgCreateReq struct {
|
||||||
|
Name string `json:"name" example:"Acme Corp"`
|
||||||
|
Domain *string `json:"domain,omitempty" example:"acme.com"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrg godoc
|
||||||
|
// @ID CreateOrg
|
||||||
|
// @Summary Create organization
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body orgCreateReq true "Org payload"
|
||||||
|
// @Success 201 {object} models.Organization
|
||||||
|
// @Failure 400 {object} utils.ErrorResponse
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Failure 409 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs [post]
|
||||||
|
// @ID createOrg
|
||||||
|
// @Security BearerAuth
|
||||||
|
func CreateOrg(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req orgCreateReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(req.Name) == "" {
|
||||||
|
utils.WriteError(w, 400, "validation_error", "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
org := models.Organization{Name: req.Name}
|
||||||
|
if req.Domain != nil && strings.TrimSpace(*req.Domain) != "" {
|
||||||
|
org.Domain = req.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&org).Error; err != nil {
|
||||||
|
utils.WriteError(w, 409, "conflict", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// creator is owner
|
||||||
|
_ = db.Create(&models.Membership{
|
||||||
|
UserID: u.ID, OrganizationID: org.ID, Role: "owner",
|
||||||
|
}).Error
|
||||||
|
|
||||||
|
utils.WriteJSON(w, 201, org)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMyOrgs godoc
|
||||||
|
// @ID ListMyOrgs
|
||||||
|
// @Summary List organizations I belong to
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} models.Organization
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs [get]
|
||||||
|
// @ID listMyOrgs
|
||||||
|
// @Security BearerAuth
|
||||||
|
func ListMyOrgs(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgs []models.Organization
|
||||||
|
if err := db.
|
||||||
|
Joins("join memberships m on m.organization_id = organizations.id").
|
||||||
|
Where("m.user_id = ?", u.ID).
|
||||||
|
Order("organizations.created_at desc").
|
||||||
|
Find(&orgs).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, 200, orgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrg godoc
|
||||||
|
// @ID GetOrg
|
||||||
|
// @Summary Get organization
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Success 200 {object} models.Organization
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Failure 404 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id} [get]
|
||||||
|
// @ID getOrg
|
||||||
|
// @Security BearerAuth
|
||||||
|
func GetOrg(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !mustMember(db, u.ID, oid) {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "not a member")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var org models.Organization
|
||||||
|
if err := db.First(&org, "id = ?", oid).Error; err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, 200, org)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgUpdateReq struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Domain *string `json:"domain,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrg godoc
|
||||||
|
// @ID UpdateOrg
|
||||||
|
// @Summary Update organization (owner/admin)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Param body body orgUpdateReq true "Update payload"
|
||||||
|
// @Success 200 {object} models.Organization
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Failure 404 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id} [patch]
|
||||||
|
// @ID updateOrg
|
||||||
|
// @Security BearerAuth
|
||||||
|
func UpdateOrg(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner", "admin"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "admin or owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req orgUpdateReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changes := map[string]any{}
|
||||||
|
if req.Name != nil {
|
||||||
|
changes["name"] = strings.TrimSpace(*req.Name)
|
||||||
|
}
|
||||||
|
if req.Domain != nil {
|
||||||
|
if d := strings.TrimSpace(*req.Domain); d == "" {
|
||||||
|
changes["domain"] = nil
|
||||||
|
} else {
|
||||||
|
changes["domain"] = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(changes) > 0 {
|
||||||
|
if err := db.Model(&models.Organization{}).Where("id = ?", oid).Updates(changes).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var out models.Organization
|
||||||
|
_ = db.First(&out, "id = ?", oid).Error
|
||||||
|
utils.WriteJSON(w, 200, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOrg godoc
|
||||||
|
// @ID DeleteOrg
|
||||||
|
// @Summary Delete organization (owner)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Success 204 "Deleted"
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Failure 404 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id} [delete]
|
||||||
|
// @ID deleteOrg
|
||||||
|
// @Security BearerAuth
|
||||||
|
func DeleteOrg(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Optional safety: deny if members >1 or resources exist; here we just delete.
|
||||||
|
res := db.Delete(&models.Organization{}, "id = ?", oid)
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Members: list/add/update/delete ----------
|
||||||
|
|
||||||
|
type memberOut struct {
|
||||||
|
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"` // owner/admin/member
|
||||||
|
}
|
||||||
|
|
||||||
|
type memberUpsertReq struct {
|
||||||
|
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
||||||
|
Role string `json:"role" example:"member"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMembers godoc
|
||||||
|
// @ID ListMembers
|
||||||
|
// @Summary List members in org
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Success 200 {array} memberOut
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/members [get]
|
||||||
|
// @ID listMembers
|
||||||
|
// @Security BearerAuth
|
||||||
|
func ListMembers(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil || !mustMember(db, u.ID, oid) {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ms []models.Membership
|
||||||
|
if err := db.Where("organization_id = ?", oid).Find(&ms).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// load emails
|
||||||
|
userIDs := make([]uuid.UUID, 0, len(ms))
|
||||||
|
for _, m := range ms {
|
||||||
|
userIDs = append(userIDs, m.UserID)
|
||||||
|
}
|
||||||
|
var emails []models.UserEmail
|
||||||
|
if len(userIDs) > 0 {
|
||||||
|
_ = db.Where("user_id in ?", userIDs).Where("is_primary = true").Find(&emails).Error
|
||||||
|
}
|
||||||
|
emailByUser := map[uuid.UUID]string{}
|
||||||
|
for _, e := range emails {
|
||||||
|
emailByUser[e.UserID] = e.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]memberOut, 0, len(ms))
|
||||||
|
for _, m := range ms {
|
||||||
|
out = append(out, memberOut{
|
||||||
|
UserID: m.UserID,
|
||||||
|
Email: emailByUser[m.UserID],
|
||||||
|
Role: m.Role,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, 200, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOrUpdateMember godoc
|
||||||
|
// @ID AddOrUpdateMember
|
||||||
|
// @Summary Add or update a member (owner/admin)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Param body body memberUpsertReq true "User & role"
|
||||||
|
// @Success 200 {object} memberOut
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/members [post]
|
||||||
|
// @ID addOrUpdateMember
|
||||||
|
// @Security BearerAuth
|
||||||
|
func AddOrUpdateMember(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner", "admin"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "admin or owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req memberUpsertReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role := strings.ToLower(strings.TrimSpace(req.Role))
|
||||||
|
if role != "owner" && role != "admin" && role != "member" {
|
||||||
|
utils.WriteError(w, 400, "validation_error", "role must be owner|admin|member")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var m models.Membership
|
||||||
|
tx := db.Where("user_id = ? AND organization_id = ?", req.UserID, oid).First(&m)
|
||||||
|
if tx.Error == nil {
|
||||||
|
// update
|
||||||
|
if err := db.Model(&m).Update("role", role).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||||
|
m = models.Membership{UserID: req.UserID, OrganizationID: oid, Role: role}
|
||||||
|
if err := db.Create(&m).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.WriteError(w, 500, "db_error", tx.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make response
|
||||||
|
var ue models.UserEmail
|
||||||
|
_ = db.Where("user_id = ? AND is_primary = true", req.UserID).First(&ue).Error
|
||||||
|
utils.WriteJSON(w, 200, memberOut{
|
||||||
|
UserID: req.UserID, Email: ue.Email, Role: m.Role,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveMember godoc
|
||||||
|
// @ID RemoveMember
|
||||||
|
// @Summary Remove a member (owner/admin)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Param user_id path string true "User ID (UUID)"
|
||||||
|
// @Success 204 "Removed"
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/members/{user_id} [delete]
|
||||||
|
// @ID removeMember
|
||||||
|
// @Security BearerAuth
|
||||||
|
func RemoveMember(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner", "admin"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "admin or owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid, err := uuid.Parse(chi.URLParam(r, "user_id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_user_id", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := db.Where("user_id = ? AND organization_id = ?", uid, oid).Delete(&models.Membership{})
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Org API Keys (key/secret pair) ----------
|
||||||
|
|
||||||
|
type orgKeyCreateReq struct {
|
||||||
|
Name string `json:"name,omitempty" example:"automation-bot"`
|
||||||
|
ExpiresInHours *int `json:"expires_in_hours,omitempty" example:"720"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgKeyCreateResp struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Scope string `json:"scope"` // "org"
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
OrgKey string `json:"org_key"` // shown once:
|
||||||
|
OrgSecret string `json:"org_secret"` // shown once:
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOrgKeys godoc
|
||||||
|
// @ID ListOrgKeys
|
||||||
|
// @Summary List org-scoped API keys (no secrets)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Success 200 {array} models.APIKey
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/api-keys [get]
|
||||||
|
// @ID listOrgKeys
|
||||||
|
// @Security BearerAuth
|
||||||
|
func ListOrgKeys(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil || !mustMember(db, u.ID, oid) {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var keys []models.APIKey
|
||||||
|
if err := db.Where("org_id = ? AND scope = ?", oid, "org").
|
||||||
|
Order("created_at desc").
|
||||||
|
Find(&keys).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// SecretHash must not be exposed; your json tags likely hide it already.
|
||||||
|
utils.WriteJSON(w, 200, keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrgKey godoc
|
||||||
|
// @ID CreateOrgKey
|
||||||
|
// @Summary Create org key/secret pair (owner/admin)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Param body body orgKeyCreateReq true "Key name + optional expiry"
|
||||||
|
// @Success 201 {object} orgKeyCreateResp
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/api-keys [post]
|
||||||
|
// @ID createOrgKey
|
||||||
|
// @Security BearerAuth
|
||||||
|
func CreateOrgKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner", "admin"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "admin or owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req orgKeyCreateReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate
|
||||||
|
keySuffix, err := randomB64URL(16)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "entropy_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sec, err := randomB64URL(32)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "entropy_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orgKey := "org_" + keySuffix
|
||||||
|
secretPlain := sec
|
||||||
|
|
||||||
|
keyHash := auth.SHA256Hex(orgKey)
|
||||||
|
secretHash, err := auth.HashSecretArgon2id(secretPlain)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 500, "hash_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var exp *time.Time
|
||||||
|
if req.ExpiresInHours != nil && *req.ExpiresInHours > 0 {
|
||||||
|
e := time.Now().Add(time.Duration(*req.ExpiresInHours) * time.Hour)
|
||||||
|
exp = &e
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := models.APIKey{
|
||||||
|
OrgID: &oid,
|
||||||
|
Scope: "org",
|
||||||
|
Name: req.Name,
|
||||||
|
KeyHash: keyHash,
|
||||||
|
SecretHash: &secretHash,
|
||||||
|
ExpiresAt: exp,
|
||||||
|
}
|
||||||
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, 201, orgKeyCreateResp{
|
||||||
|
ID: rec.ID,
|
||||||
|
Name: rec.Name,
|
||||||
|
Scope: rec.Scope,
|
||||||
|
CreatedAt: rec.CreatedAt,
|
||||||
|
ExpiresAt: rec.ExpiresAt,
|
||||||
|
OrgKey: orgKey,
|
||||||
|
OrgSecret: secretPlain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOrgKey godoc
|
||||||
|
// @ID DeleteOrgKey
|
||||||
|
// @Summary Delete org key (owner/admin)
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Org ID (UUID)"
|
||||||
|
// @Param key_id path string true "Key ID (UUID)"
|
||||||
|
// @Success 204 "Deleted"
|
||||||
|
// @Failure 401 {object} utils.ErrorResponse
|
||||||
|
// @Router /orgs/{id}/api-keys/{key_id} [delete]
|
||||||
|
// @ID deleteOrgKey
|
||||||
|
// @Security BearerAuth
|
||||||
|
func DeleteOrgKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := mustUser(r)
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, 401, "unauthorized", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 404, "not_found", "org not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok, _ := isOrgRole(db, u.ID, oid, "owner", "admin"); !ok {
|
||||||
|
utils.WriteError(w, 401, "forbidden", "admin or owner required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kid, err := uuid.Parse(chi.URLParam(r, "key_id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, 400, "invalid_key_id", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := db.Where("id = ? AND org_id = ? AND scope = ?", kid, oid, "org").Delete(&models.APIKey{})
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.WriteError(w, 500, "db_error", res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
utils.WriteError(w, 404, "not_found", "key not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
}
|
||||||
388
internal/handlers/servers.go
Normal file
388
internal/handlers/servers.go
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListServers godoc
|
||||||
|
// @ID ListServers
|
||||||
|
// @Summary List servers (org scoped)
|
||||||
|
// @Description Returns servers for the organization in X-Org-ID. Optional filters: status, role.
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param status query string false "Filter by status (pending|provisioning|ready|failed)"
|
||||||
|
// @Param role query string false "Filter by role"
|
||||||
|
// @Success 200 {array} dto.ServerResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "failed to list servers"
|
||||||
|
// @Router /servers [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListServers(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := db.Where("organization_id = ?", orgID)
|
||||||
|
|
||||||
|
if s := strings.TrimSpace(r.URL.Query().Get("status")); s != "" {
|
||||||
|
if !validStatus(s) {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "status_invalid", "invalid status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q = q.Where("status = ?", strings.ToLower(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
if role := strings.TrimSpace(r.URL.Query().Get("role")); role != "" {
|
||||||
|
q = q.Where("role = ?", role)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.Server
|
||||||
|
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to list servers")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]dto.ServerResponse, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, dto.ServerResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
OrganizationID: row.OrganizationID,
|
||||||
|
Hostname: row.Hostname,
|
||||||
|
PublicIPAddress: row.PublicIPAddress,
|
||||||
|
PrivateIPAddress: row.PrivateIPAddress,
|
||||||
|
SSHUser: row.SSHUser,
|
||||||
|
SshKeyID: row.SshKeyID,
|
||||||
|
Role: row.Role,
|
||||||
|
Status: row.Status,
|
||||||
|
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServer godoc
|
||||||
|
// @ID GetServer
|
||||||
|
// @Summary Get server by ID (org scoped)
|
||||||
|
// @Description Returns one server in the given organization.
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Server ID (UUID)"
|
||||||
|
// @Success 200 {object} dto.ServerResponse
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "fetch failed"
|
||||||
|
// @Router /servers/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetServer(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.Server
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServer godoc
|
||||||
|
// @ID CreateServer
|
||||||
|
// @Summary Create server (org scoped)
|
||||||
|
// @Description Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param body body dto.CreateServerRequest true "Server payload"
|
||||||
|
// @Success 201 {object} dto.ServerResponse
|
||||||
|
// @Failure 400 {string} string "invalid json / missing fields / invalid status / invalid ssh_key_id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "create failed"
|
||||||
|
// @Router /servers [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateServer(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.CreateServerRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Role = strings.ToLower(strings.TrimSpace(req.Role))
|
||||||
|
req.Status = strings.ToLower(strings.TrimSpace(req.Status))
|
||||||
|
pub := strings.TrimSpace(req.PublicIPAddress)
|
||||||
|
|
||||||
|
if req.PrivateIPAddress == "" || req.SSHUser == "" || req.SshKeyID == "" || req.Role == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "private_ip_address, ssh_user, ssh_key_id and role are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "" && !validStatus(req.Status) {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "status_invalid", "invalid status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Role == "bastion" && pub == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "public_ip_required", "public_ip_address is required for role=bastion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID, err := uuid.Parse(req.SshKeyID)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid ssh_key_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureKeyBelongsToOrg(orgID, keyID, db); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid or unauthorized ssh_key_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicPtr *string
|
||||||
|
if pub != "" {
|
||||||
|
publicPtr = &pub
|
||||||
|
}
|
||||||
|
|
||||||
|
s := models.Server{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Hostname: req.Hostname,
|
||||||
|
PublicIPAddress: publicPtr,
|
||||||
|
PrivateIPAddress: req.PrivateIPAddress,
|
||||||
|
SSHUser: req.SSHUser,
|
||||||
|
SshKeyID: keyID,
|
||||||
|
Role: req.Role,
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
s.Status = strings.ToLower(req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&s).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to create server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateServer godoc
|
||||||
|
// @ID UpdateServer
|
||||||
|
// @Summary Update server (org scoped)
|
||||||
|
// @Description Partially update fields; changing ssh_key_id validates ownership.
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Server ID (UUID)"
|
||||||
|
// @Param body body dto.UpdateServerRequest true "Fields to update"
|
||||||
|
// @Success 200 {object} dto.ServerResponse
|
||||||
|
// @Failure 400 {string} string "invalid id / invalid json / invalid status / invalid ssh_key_id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "update failed"
|
||||||
|
// @Router /servers/{id} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func UpdateServer(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var server models.Server
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&server).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateServerRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next := server
|
||||||
|
|
||||||
|
if req.Hostname != nil {
|
||||||
|
next.Hostname = *req.Hostname
|
||||||
|
}
|
||||||
|
if req.PrivateIPAddress != nil {
|
||||||
|
next.PrivateIPAddress = *req.PrivateIPAddress
|
||||||
|
}
|
||||||
|
if req.PublicIPAddress != nil {
|
||||||
|
next.PublicIPAddress = req.PublicIPAddress
|
||||||
|
}
|
||||||
|
if req.SSHUser != nil {
|
||||||
|
next.SSHUser = *req.SSHUser
|
||||||
|
}
|
||||||
|
if req.Role != nil {
|
||||||
|
next.Role = *req.Role
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
st := strings.ToLower(strings.TrimSpace(*req.Status))
|
||||||
|
if !validStatus(st) {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "status_invalid", "invalid status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.Status = st
|
||||||
|
}
|
||||||
|
if req.SshKeyID != nil {
|
||||||
|
keyID, err := uuid.Parse(*req.SshKeyID)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid ssh_key_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureKeyBelongsToOrg(orgID, keyID, db); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid or unauthorized ssh_key_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.SshKeyID = keyID
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(next.Role, "bastion") &&
|
||||||
|
(next.PublicIPAddress == nil || strings.TrimSpace(*next.PublicIPAddress) == "") {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "public_ip_required", "public_ip_address is required for role=bastion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Save(&next).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to update server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteServer godoc
|
||||||
|
// @ID DeleteServer
|
||||||
|
// @Summary Delete server (org scoped)
|
||||||
|
// @Description Permanently deletes the server.
|
||||||
|
// @Tags Servers
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Server ID (UUID)"
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "delete failed"
|
||||||
|
// @Router /servers/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteServer(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&models.Server{}).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Delete(&models.Server{}).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to delete server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
func validStatus(status string) bool {
|
||||||
|
switch strings.ToLower(status) {
|
||||||
|
case "pending", "provisioning", "ready", "failed", "":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureKeyBelongsToOrg(orgID, keyID uuid.UUID, db *gorm.DB) error {
|
||||||
|
var k models.SshKey
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", keyID, orgID).First(&k).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("ssh key not found for this organization")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
553
internal/handlers/ssh.go
Normal file
553
internal/handlers/ssh.go
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListPublicSshKeys godoc
|
||||||
|
// @ID ListPublicSshKeys
|
||||||
|
// @Summary List ssh keys (org scoped)
|
||||||
|
// @Description Returns ssh keys for the organization in X-Org-ID.
|
||||||
|
// @Tags Ssh
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Success 200 {array} dto.SshResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "failed to list keys"
|
||||||
|
// @Router /ssh [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListPublicSshKeys(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.SshKey
|
||||||
|
if err := db.Where("organization_id = ?", orgID).Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to list ssh keys")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]dto.SshResponse, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, dto.SshResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
OrganizationID: row.OrganizationID,
|
||||||
|
Name: row.Name,
|
||||||
|
PublicKey: row.PublicKey,
|
||||||
|
Fingerprint: row.Fingerprint,
|
||||||
|
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSSHKey
|
||||||
|
// @ID CreateSSHKey
|
||||||
|
// @Summary Create ssh keypair (org scoped)
|
||||||
|
// @Description Generates an RSA or ED25519 keypair, saves it, and returns metadata. For RSA you may set bits (2048/3072/4096). Default is 4096. ED25519 ignores bits.
|
||||||
|
// @Tags Ssh
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param body body dto.CreateSSHRequest true "Key generation options"
|
||||||
|
// @Success 201 {object} dto.SshResponse
|
||||||
|
// @Failure 400 {string} string "invalid json / invalid bits"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "generation/create failed"
|
||||||
|
// @Router /ssh [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.CreateSSHRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid JSON payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyType := "rsa"
|
||||||
|
if req.Type != nil && strings.TrimSpace(*req.Type) != "" {
|
||||||
|
keyType = strings.ToLower(strings.TrimSpace(*req.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyType != "rsa" && keyType != "ed25519" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_type", "invalid type (rsa|ed25519)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
privPEM string
|
||||||
|
pubAuth string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch keyType {
|
||||||
|
case "rsa":
|
||||||
|
bits := 4096
|
||||||
|
if req.Bits != nil {
|
||||||
|
if !allowedBits(*req.Bits) {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_bits", "invalid bits (allowed: 2048, 3072, 4096)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bits = *req.Bits
|
||||||
|
}
|
||||||
|
privPEM, pubAuth, err = GenerateRSAPEMAndAuthorized(bits, strings.TrimSpace(req.Comment))
|
||||||
|
|
||||||
|
case "ed25519":
|
||||||
|
if req.Bits != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_bits_for_type", "bits is only valid for RSA")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
privPEM, pubAuth, err = GenerateEd25519PEMAndAuthorized(strings.TrimSpace(req.Comment))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "keygen_failure", "key generation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher, iv, tag, err := utils.EncryptForOrg(orgID, []byte(privPEM), db)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "encryption failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubAuth))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "ssh_failure", "ssh public key parsing failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := ssh.FingerprintSHA256(parsed)
|
||||||
|
|
||||||
|
key := models.SshKey{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Name: req.Name,
|
||||||
|
PublicKey: pubAuth,
|
||||||
|
EncryptedPrivateKey: cipher,
|
||||||
|
PrivateIV: iv,
|
||||||
|
PrivateTag: tag,
|
||||||
|
Fingerprint: fp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&key).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to create ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, dto.SshResponse{
|
||||||
|
ID: key.ID,
|
||||||
|
OrganizationID: key.OrganizationID,
|
||||||
|
Name: key.Name,
|
||||||
|
PublicKey: key.PublicKey,
|
||||||
|
Fingerprint: key.Fingerprint,
|
||||||
|
CreatedAt: key.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: key.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSSHKey godoc
|
||||||
|
// @ID GetSSHKey
|
||||||
|
// @Summary Get ssh key by ID (org scoped)
|
||||||
|
// @Description Returns public key fields. Append `?reveal=true` to include the private key PEM.
|
||||||
|
// @Tags Ssh
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "SSH Key ID (UUID)"
|
||||||
|
// @Param reveal query bool false "Reveal private key PEM"
|
||||||
|
// @Success 200 {object} dto.SshResponse
|
||||||
|
// @Success 200 {object} dto.SshRevealResponse "When reveal=true"
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "fetch failed"
|
||||||
|
// @Router /ssh/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_key_id", "invalid SSH Key ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var key models.SshKey
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&key).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "ssh_key_not_found", "ssh key not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("reveal") != "true" {
|
||||||
|
utils.WriteJSON(w, http.StatusOK, dto.SshResponse{
|
||||||
|
ID: key.ID,
|
||||||
|
OrganizationID: key.OrganizationID,
|
||||||
|
Name: key.Name,
|
||||||
|
PublicKey: key.PublicKey,
|
||||||
|
Fingerprint: key.Fingerprint,
|
||||||
|
CreatedAt: key.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: key.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plain, err := utils.DecryptForOrg(orgID, key.EncryptedPrivateKey, key.PrivateIV, key.PrivateTag, db)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to decrypt ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WriteJSON(w, http.StatusOK, dto.SshRevealResponse{
|
||||||
|
SshResponse: dto.SshResponse{
|
||||||
|
ID: key.ID,
|
||||||
|
OrganizationID: key.OrganizationID,
|
||||||
|
Name: key.Name,
|
||||||
|
PublicKey: key.PublicKey,
|
||||||
|
Fingerprint: key.Fingerprint,
|
||||||
|
CreatedAt: key.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: key.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
PrivateKey: plain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSSHKey godoc
|
||||||
|
// @ID DeleteSSHKey
|
||||||
|
// @Summary Delete ssh keypair (org scoped)
|
||||||
|
// @Description Permanently deletes a keypair.
|
||||||
|
// @Tags Ssh
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "SSH Key ID (UUID)"
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "delete failed"
|
||||||
|
// @Router /ssh/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_key_id", "invalid SSH Key ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).
|
||||||
|
Delete(&models.SshKey{}).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to delete ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadSSHKey godoc
|
||||||
|
// @ID DownloadSSHKey
|
||||||
|
// @Summary Download ssh key files by ID (org scoped)
|
||||||
|
// @Description Download `part=public|private|both` of the keypair. `both` returns a zip file.
|
||||||
|
// @Tags Ssh
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "SSH Key ID (UUID)"
|
||||||
|
// @Param part query string true "Which part to download" Enums(public,private,both)
|
||||||
|
// @Success 200 {string} string "file content"
|
||||||
|
// @Failure 400 {string} string "invalid id / invalid part"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "download failed"
|
||||||
|
// @Router /ssh/{id}/download [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DownloadSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_key_id", "invalid SSH Key ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var key models.SshKey
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).
|
||||||
|
First(&key).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "ssh_key_not_found", "ssh key not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
part := strings.ToLower(r.URL.Query().Get("part"))
|
||||||
|
if part == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_part", "invalid part (public|private|both)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := strings.ToLower(r.URL.Query().Get("mode"))
|
||||||
|
if mode != "" && mode != "json" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_mode", "invalid mode (json|attachment[default])")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == "json" {
|
||||||
|
resp := dto.SshMaterialJSON{
|
||||||
|
ID: key.ID.String(),
|
||||||
|
Name: key.Name,
|
||||||
|
Fingerprint: key.Fingerprint,
|
||||||
|
}
|
||||||
|
switch part {
|
||||||
|
case "public":
|
||||||
|
pub := key.PublicKey
|
||||||
|
resp.PublicKey = &pub
|
||||||
|
resp.Filenames = []string{fmt.Sprintf("id_rsa_%s.pub", key.ID.String())}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, resp)
|
||||||
|
return
|
||||||
|
|
||||||
|
case "private":
|
||||||
|
plain, err := utils.DecryptForOrg(orgID, key.EncryptedPrivateKey, key.PrivateIV, key.PrivateTag, db)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to decrypt ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.PrivatePEM = &plain
|
||||||
|
resp.Filenames = []string{fmt.Sprintf("id_rsa_%s.pem", key.ID.String())}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, resp)
|
||||||
|
return
|
||||||
|
|
||||||
|
case "both":
|
||||||
|
plain, err := utils.DecryptForOrg(orgID, key.EncryptedPrivateKey, key.PrivateIV, key.PrivateTag, db)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to decrypt ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
_ = toZipFile(fmt.Sprintf("id_rsa_%s.pem", key.ID.String()), []byte(plain), zw)
|
||||||
|
_ = toZipFile(fmt.Sprintf("id_rsa_%s.pub", key.ID.String()), []byte(key.PublicKey), zw)
|
||||||
|
_ = zw.Close()
|
||||||
|
|
||||||
|
b64 := utils.EncodeB64(buf.Bytes())
|
||||||
|
resp.ZipBase64 = &b64
|
||||||
|
resp.Filenames = []string{
|
||||||
|
fmt.Sprintf("id_rsa_%s.zip", key.ID.String()),
|
||||||
|
fmt.Sprintf("id_rsa_%s.pem", key.ID.String()),
|
||||||
|
fmt.Sprintf("id_rsa_%s.pub", key.ID.String()),
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, resp)
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_part", "invalid part (public|private|both)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := keyFilenamePrefix(key.PublicKey)
|
||||||
|
|
||||||
|
switch part {
|
||||||
|
case "public":
|
||||||
|
filename := fmt.Sprintf("%s_%s.pub", prefix, key.ID.String())
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
_, _ = w.Write([]byte(key.PublicKey))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "private":
|
||||||
|
plain, err := utils.DecryptForOrg(orgID, key.EncryptedPrivateKey, key.PrivateIV, key.PrivateTag, db)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to decrypt ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%s_%s.pem", prefix, key.ID.String())
|
||||||
|
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
_, _ = w.Write([]byte(plain))
|
||||||
|
return
|
||||||
|
|
||||||
|
case "both":
|
||||||
|
plain, err := utils.DecryptForOrg(orgID, key.EncryptedPrivateKey, key.PrivateIV, key.PrivateTag, db)
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to decrypt ssh key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
_ = toZipFile(fmt.Sprintf("%s_%s.pem", prefix, key.ID.String()), []byte(plain), zw)
|
||||||
|
_ = toZipFile(fmt.Sprintf("%s_%s.pub", prefix, key.ID.String()), []byte(key.PublicKey), zw)
|
||||||
|
_ = zw.Close()
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("ssh_key_%s.zip", key.ID.String())
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
_, _ = w.Write(buf.Bytes())
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "invalid_ssh_part", "invalid part (public|private|both)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
func allowedBits(b int) bool {
|
||||||
|
return b == 2048 || b == 3072 || b == 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRSA(bits int) (*rsa.PrivateKey, error) {
|
||||||
|
return rsa.GenerateKey(rand.Reader, bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RSAPrivateToPEMAndAuthorized(priv *rsa.PrivateKey, comment string) (privPEM string, authorized string, err error) {
|
||||||
|
der := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: der}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err = pem.Encode(&buf, block); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
auth := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pub)))
|
||||||
|
comment = strings.TrimSpace(comment)
|
||||||
|
if comment != "" {
|
||||||
|
auth += " " + comment
|
||||||
|
}
|
||||||
|
return buf.String(), auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRSAPEMAndAuthorized(bits int, comment string) (string, string, error) {
|
||||||
|
priv, err := GenerateRSA(bits)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return RSAPrivateToPEMAndAuthorized(priv, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toZipFile(filename string, content []byte, zw *zip.Writer) error {
|
||||||
|
f, err := zw.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = f.Write(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyFilenamePrefix(pubAuth string) string {
|
||||||
|
// OpenSSH authorized keys start with the algorithm name
|
||||||
|
if strings.HasPrefix(pubAuth, "ssh-ed25519 ") {
|
||||||
|
return "id_ed25519"
|
||||||
|
}
|
||||||
|
// default to RSA
|
||||||
|
return "id_rsa"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateEd25519PEMAndAuthorized(comment string) (privPEM string, authorized string, err error) {
|
||||||
|
// Generate ed25519 keypair
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private: PKCS#8 PEM
|
||||||
|
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
block := &pem.Block{Type: "PRIVATE KEY", Bytes: der}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := pem.Encode(&buf, block); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public: OpenSSH authorized_key
|
||||||
|
sshPub, err := ssh.NewPublicKey(ed25519.PublicKey(pub))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
auth := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub)))
|
||||||
|
comment = strings.TrimSpace(comment)
|
||||||
|
if comment != "" {
|
||||||
|
auth += " " + comment
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), auth, nil
|
||||||
|
}
|
||||||
335
internal/handlers/taints.go
Normal file
335
internal/handlers/taints.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/glueops/autoglue/internal/utils"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListTaints godoc
|
||||||
|
// @ID ListTaints
|
||||||
|
// @Summary List node pool taints (org scoped)
|
||||||
|
// @Description Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.
|
||||||
|
// @Tags Taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param key query string false "Exact key"
|
||||||
|
// @Param value query string false "Exact value"
|
||||||
|
// @Param q query string false "key contains (case-insensitive)"
|
||||||
|
// @Success 200 {array} dto.TaintResponse
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "failed to list node taints"
|
||||||
|
// @Router /taints [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func ListTaints(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := db.Where("organization_id = ?", orgID)
|
||||||
|
|
||||||
|
if key := strings.TrimSpace(r.URL.Query().Get("key")); key != "" {
|
||||||
|
q = q.Where(`key = ?`, key)
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(r.URL.Query().Get("value")); val != "" {
|
||||||
|
q = q.Where(`value = ?`, val)
|
||||||
|
}
|
||||||
|
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||||
|
q = q.Where(`key ILIKE ?`, "%"+needle+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.Taint
|
||||||
|
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]dto.TaintResponse, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, dto.TaintResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
Key: row.Key,
|
||||||
|
Value: row.Value,
|
||||||
|
Effect: row.Effect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaint godoc
|
||||||
|
// @ID GetTaint
|
||||||
|
// @Summary Get node taint by ID (org scoped)
|
||||||
|
// @Tags Taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Node Taint ID (UUID)"
|
||||||
|
// @Success 200 {object} dto.TaintResponse
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "fetch failed"
|
||||||
|
// @Router /taints/{id} [get]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func GetTaint(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.Taint
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := dto.TaintResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
Key: row.Key,
|
||||||
|
Value: row.Value,
|
||||||
|
Effect: row.Effect,
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTaint godoc
|
||||||
|
// @ID CreateTaint
|
||||||
|
// @Summary Create node taint (org scoped)
|
||||||
|
// @Description Creates a taint.
|
||||||
|
// @Tags Taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param body body dto.CreateTaintRequest true "Taint payload"
|
||||||
|
// @Success 201 {object} dto.TaintResponse
|
||||||
|
// @Failure 400 {string} string "invalid json / missing fields / invalid node_pool_ids"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "create failed"
|
||||||
|
// @Router /taints [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func CreateTaint(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.CreateTaintRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Key = strings.TrimSpace(req.Key)
|
||||||
|
req.Value = strings.TrimSpace(req.Value)
|
||||||
|
req.Effect = strings.TrimSpace(req.Effect)
|
||||||
|
|
||||||
|
if req.Key == "" || req.Value == "" || req.Effect == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "missing key/value/effect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := allowedEffects[req.Effect]; !ok {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid effect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t := models.Taint{
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Key: req.Key,
|
||||||
|
Value: req.Value,
|
||||||
|
Effect: req.Effect,
|
||||||
|
}
|
||||||
|
if err := db.Create(&t).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := dto.TaintResponse{
|
||||||
|
ID: t.ID,
|
||||||
|
Key: t.Key,
|
||||||
|
Value: t.Value,
|
||||||
|
Effect: t.Effect,
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTaint godoc
|
||||||
|
// @ID UpdateTaint
|
||||||
|
// @Summary Update node taint (org scoped)
|
||||||
|
// @Description Partially update taint fields.
|
||||||
|
// @Tags Taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Node Taint ID (UUID)"
|
||||||
|
// @Param body body dto.UpdateTaintRequest true "Fields to update"
|
||||||
|
// @Success 200 {object} dto.TaintResponse
|
||||||
|
// @Failure 400 {string} string "invalid id / invalid json"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "update failed"
|
||||||
|
// @Router /taints/{id} [patch]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func UpdateTaint(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var t models.Taint
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&t).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateTaintRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next := t
|
||||||
|
|
||||||
|
if req.Key != nil {
|
||||||
|
next.Key = strings.TrimSpace(*req.Key)
|
||||||
|
}
|
||||||
|
if req.Value != nil {
|
||||||
|
next.Value = strings.TrimSpace(*req.Value)
|
||||||
|
}
|
||||||
|
if req.Effect != nil {
|
||||||
|
e := strings.TrimSpace(*req.Effect)
|
||||||
|
if e == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "missing effect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := allowedEffects[e]; !ok {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid effect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.Effect = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Save(&next).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := dto.TaintResponse{
|
||||||
|
ID: next.ID,
|
||||||
|
Key: next.Key,
|
||||||
|
Value: next.Value,
|
||||||
|
Effect: next.Effect,
|
||||||
|
}
|
||||||
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTaint godoc
|
||||||
|
// @ID DeleteTaint
|
||||||
|
// @Summary Delete taint (org scoped)
|
||||||
|
// @Description Permanently deletes the taint.
|
||||||
|
// @Tags Taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param id path string true "Node Taint ID (UUID)"
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Failure 400 {string} string "invalid id"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
|
// @Failure 403 {string} string "organization required"
|
||||||
|
// @Failure 500 {string} string "delete failed"
|
||||||
|
// @Router /taints/{id} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DeleteTaint(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||||
|
if !ok {
|
||||||
|
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var row models.Taint
|
||||||
|
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "not_found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Delete(&row).Error; err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
var allowedEffects = map[string]struct{}{
|
||||||
|
"NoSchedule": {},
|
||||||
|
"PreferNoSchedule": {},
|
||||||
|
"NoExecute": {},
|
||||||
|
}
|
||||||
35
internal/keys/base64util.go
Normal file
35
internal/keys/base64util.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decode32ByteKey(s string) ([]byte, error) {
|
||||||
|
try := func(enc *base64.Encoding, v string) ([]byte, bool) {
|
||||||
|
if b, err := enc.DecodeString(v); err == nil && len(b) == 32 {
|
||||||
|
return b, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try raw (no padding) variants first
|
||||||
|
if b, ok := try(base64.RawURLEncoding, s); ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
if b, ok := try(base64.RawStdEncoding, s); ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try padded variants (add padding if missing)
|
||||||
|
pad := func(v string) string { return v + strings.Repeat("=", (4-len(v)%4)%4) }
|
||||||
|
if b, ok := try(base64.URLEncoding, pad(s)); ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
if b, ok := try(base64.StdEncoding, pad(s)); ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("key must be 32 bytes in base64/base64url")
|
||||||
|
}
|
||||||
5
internal/keys/export.go
Normal file
5
internal/keys/export.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
func Decrypt(encKeyB64, enc string) ([]byte, error) {
|
||||||
|
return decryptAESGCM(encKeyB64, enc)
|
||||||
|
}
|
||||||
149
internal/keys/keys.go
Normal file
149
internal/keys/keys.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenOpts struct {
|
||||||
|
Alg string // "RS256"|"RS384"|"RS512"|"EdDSA"
|
||||||
|
Bits int // RSA bits (2048/3072/4096). ignored for EdDSA
|
||||||
|
KID string // optional; if empty we generate one
|
||||||
|
NBF *time.Time
|
||||||
|
EXP *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateAndStore(db *gorm.DB, encKeyB64 string, opts GenOpts) (*models.SigningKey, error) {
|
||||||
|
if opts.KID == "" {
|
||||||
|
opts.KID = uuid.NewString()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubPEM, privPEM []byte
|
||||||
|
var alg = opts.Alg
|
||||||
|
|
||||||
|
switch alg {
|
||||||
|
case "RS256", "RS384", "RS512":
|
||||||
|
if opts.Bits == 0 {
|
||||||
|
opts.Bits = 3072
|
||||||
|
}
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, opts.Bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privDER := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDER})
|
||||||
|
|
||||||
|
pubDER := x509.MarshalPKCS1PublicKey(&priv.PublicKey)
|
||||||
|
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubDER})
|
||||||
|
|
||||||
|
case "EdDSA":
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})
|
||||||
|
|
||||||
|
pubDER, err := x509.MarshalPKIXPublicKey(pub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported alg: %s", alg)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateOut := string(privPEM)
|
||||||
|
if encKeyB64 != "" {
|
||||||
|
enc, err := encryptAESGCM(encKeyB64, privPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateOut = enc
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := models.SigningKey{
|
||||||
|
Kid: opts.KID,
|
||||||
|
Alg: alg,
|
||||||
|
Use: "sig",
|
||||||
|
IsActive: true,
|
||||||
|
PublicPEM: string(pubPEM),
|
||||||
|
PrivatePEM: privateOut,
|
||||||
|
NotBefore: opts.NBF,
|
||||||
|
ExpiresAt: opts.EXP,
|
||||||
|
}
|
||||||
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptAESGCM(b64 string, plaintext []byte) (string, error) {
|
||||||
|
key, err := decode32ByteKey(b64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(key) != 32 {
|
||||||
|
return "", errors.New("JWT_PRIVATE_ENC_KEY must be 32 bytes (base64url)")
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
nonce := make([]byte, aead.NonceSize())
|
||||||
|
if _, err = rand.Read(nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out := aead.Seal(nonce, nonce, plaintext, nil)
|
||||||
|
return "enc:aesgcm:" + base64.RawStdEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptAESGCM(b64 string, enc string) ([]byte, error) {
|
||||||
|
if !bytes.HasPrefix([]byte(enc), []byte("enc:aesgcm:")) {
|
||||||
|
return nil, errors.New("not encrypted")
|
||||||
|
}
|
||||||
|
key, err := decode32ByteKey(b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blob, err := base64.RawStdEncoding.DecodeString(enc[len("enc:aesgcm:"):])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonceSize := aead.NonceSize()
|
||||||
|
if len(blob) < nonceSize {
|
||||||
|
return nil, errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
nonce, ct := blob[:nonceSize], blob[nonceSize:]
|
||||||
|
return aead.Open(nil, nonce, ct, nil)
|
||||||
|
}
|
||||||
23
internal/models/account.go
Normal file
23
internal/models/account.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
// example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
UserID uuid.UUID `gorm:"index;not null" json:"user_id" format:"uuid"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||||
|
Provider string `gorm:"not null" json:"provider"`
|
||||||
|
Subject string `gorm:"not null" json:"subject"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
EmailVerified bool `gorm:"not null;default:false" json:"email_verified"`
|
||||||
|
Profile datatypes.JSON `gorm:"type:jsonb;not null;default:'{}'" json:"profile"`
|
||||||
|
SecretHash *string `json:"-"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamptz;column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
23
internal/models/api_key.go
Normal file
23
internal/models/api_key.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIKey struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
Name string `gorm:"not null;default:''" json:"name"`
|
||||||
|
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||||
|
Scope string `gorm:"not null;default:''" json:"scope"`
|
||||||
|
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
|
||||||
|
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
|
||||||
|
SecretHash *string `json:"-"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
|
||||||
|
Revoked bool `gorm:"not null;default:false" json:"revoked"`
|
||||||
|
Prefix *string `json:"prefix,omitempty"`
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
15
internal/models/master_key.go
Normal file
15
internal/models/master_key.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterKey struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||||
|
Key string `gorm:"not null"`
|
||||||
|
IsActive bool `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
|
||||||
|
}
|
||||||
18
internal/models/membership.go
Normal file
18
internal/models/membership.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Membership struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
UserID uuid.UUID `gorm:"index;not null" json:"user_id" format:"uuid"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"index;not null" json:"org_id" format:"uuid"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`
|
||||||
|
Role string `gorm:"not null;default:'member'" json:"role"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
21
internal/models/node_pool.go
Normal file
21
internal/models/node_pool.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodePool struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Servers []Server `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"servers,omitempty"`
|
||||||
|
//Annotations []Annotation `gorm:"many2many:node_annotations;constraint:OnDelete:CASCADE" json:"annotations,omitempty"`
|
||||||
|
//Labels []Label `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"labels,omitempty"`
|
||||||
|
Taints []Taint `gorm:"many2many:node_taints;constraint:OnDelete:CASCADE" json:"taints,omitempty"`
|
||||||
|
//Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
20
internal/models/organization-key.go
Normal file
20
internal/models/organization-key.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrganizationKey struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
MasterKeyID uuid.UUID `gorm:"type:uuid;not null"`
|
||||||
|
MasterKey MasterKey `gorm:"foreignKey:MasterKeyID;constraint:OnDelete:CASCADE" json:"master_key"`
|
||||||
|
EncryptedKey string `gorm:"not null"`
|
||||||
|
IV string `gorm:"not null"`
|
||||||
|
Tag string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
16
internal/models/organization.go
Normal file
16
internal/models/organization.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Organization struct {
|
||||||
|
// example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Domain *string `gorm:"index" json:"domain"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
17
internal/models/refresh_token.go
Normal file
17
internal/models/refresh_token.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
||||||
|
UserID uuid.UUID `gorm:"index;not null" json:"user_id"`
|
||||||
|
FamilyID uuid.UUID `gorm:"type:uuid;index;not null" json:"family_id"`
|
||||||
|
TokenHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||||
|
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
|
||||||
|
RevokedAt *time.Time `json:"revoked_at"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||||
|
}
|
||||||
36
internal/models/server.go
Normal file
36
internal/models/server.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
PublicIPAddress *string `json:"public_ip_address,omitempty"`
|
||||||
|
PrivateIPAddress string `gorm:"not null" json:"private_ip_address"`
|
||||||
|
SSHUser string `gorm:"not null" json:"ssh_user"`
|
||||||
|
SshKeyID uuid.UUID `gorm:"type:uuid;not null" json:"ssh_key_id"`
|
||||||
|
SshKey SshKey `gorm:"foreignKey:SshKeyID" json:"ssh_key"`
|
||||||
|
Role string `gorm:"not null" json:"role"` // e.g., "master", "worker", "bastion"
|
||||||
|
Status string `gorm:"default:'pending'" json:"status"` // pending, provisioning, ready, failed
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) BeforeSave(tx *gorm.DB) error {
|
||||||
|
role := strings.ToLower(strings.TrimSpace(s.Role))
|
||||||
|
if role == "bastion" {
|
||||||
|
if s.PublicIPAddress == nil || strings.TrimSpace(*s.PublicIPAddress) == "" {
|
||||||
|
return errors.New("public_ip_address is required for role=bastion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
internal/models/signing_key.go
Normal file
22
internal/models/signing_key.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SigningKey struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
|
||||||
|
Kid string `gorm:"uniqueIndex;not null" json:"kid"` // key id (header 'kid')
|
||||||
|
Alg string `gorm:"not null" json:"alg"` // RS256|RS384|RS512|EdDSA
|
||||||
|
Use string `gorm:"not null;default:'sig'" json:"use"` // "sig"
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
PublicPEM string `gorm:"type:text;not null" json:"-"`
|
||||||
|
PrivatePEM string `gorm:"type:text;not null" json:"-"`
|
||||||
|
NotBefore *time.Time `json:"-"`
|
||||||
|
ExpiresAt *time.Time `json:"-"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
|
||||||
|
RotatedFrom *uuid.UUID `json:"-"` // previous key id, if any
|
||||||
|
}
|
||||||
21
internal/models/ssh-key.go
Normal file
21
internal/models/ssh-key.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SshKey struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
PublicKey string `gorm:"not null"`
|
||||||
|
EncryptedPrivateKey string `gorm:"not null"`
|
||||||
|
PrivateIV string `gorm:"not null"`
|
||||||
|
PrivateTag string `gorm:"not null"`
|
||||||
|
Fingerprint string `gorm:"not null;index" json:"fingerprint"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
18
internal/models/taint.go
Normal file
18
internal/models/taint.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Taint struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Key string `gorm:"not null" json:"key"`
|
||||||
|
Value string `gorm:"not null" json:"value"`
|
||||||
|
Effect string `gorm:"not null" json:"effect"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
18
internal/models/user.go
Normal file
18
internal/models/user.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
// example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
|
PrimaryEmail *string `json:"primary_email,omitempty"`
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||||
|
IsDisabled bool `json:"is_disabled"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
19
internal/models/user_email.go
Normal file
19
internal/models/user_email.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserEmail struct {
|
||||||
|
// example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
|
UserID uuid.UUID `gorm:"index;not null" json:"user_id" format:"uuid"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user"`
|
||||||
|
Email string `gorm:"not null" json:"email"`
|
||||||
|
IsVerified bool `gorm:"not null;default:false" json:"is_verified"`
|
||||||
|
IsPrimary bool `gorm:"not null;default:false" json:"is_primary"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
85
internal/utils/crypto.go
Normal file
85
internal/utils/crypto.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoActiveMasterKey = errors.New("no active master key found")
|
||||||
|
ErrInvalidOrgID = errors.New("invalid organization ID")
|
||||||
|
ErrCredentialNotFound = errors.New("credential not found")
|
||||||
|
ErrInvalidMasterKeyLen = errors.New("invalid master key length")
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||||
|
return nil, fmt.Errorf("rand: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptAESGCM(plaintext, key []byte) (cipherNoTag, iv, tag []byte, _ error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("gcm: %w", err)
|
||||||
|
}
|
||||||
|
if gcm.NonceSize() != 12 {
|
||||||
|
return nil, nil, nil, fmt.Errorf("unexpected nonce size: %d", gcm.NonceSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
iv, err = randomBytes(gcm.NonceSize())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go’s GCM returns ciphertext||tag, with 16-byte tag.
|
||||||
|
cipherWithTag := gcm.Seal(nil, iv, plaintext, nil)
|
||||||
|
if len(cipherWithTag) < 16 {
|
||||||
|
return nil, nil, nil, errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
tagLen := 16
|
||||||
|
cipherNoTag = cipherWithTag[:len(cipherWithTag)-tagLen]
|
||||||
|
tag = cipherWithTag[len(cipherWithTag)-tagLen:]
|
||||||
|
return cipherNoTag, iv, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptAESGCM(cipherNoTag, key, iv, tag []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cipher: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gcm: %w", err)
|
||||||
|
}
|
||||||
|
if gcm.NonceSize() != len(iv) {
|
||||||
|
return nil, fmt.Errorf("bad nonce size: %d", len(iv))
|
||||||
|
}
|
||||||
|
// Reattach tag
|
||||||
|
cipherWithTag := append(append([]byte{}, cipherNoTag...), tag...)
|
||||||
|
plain, err := gcm.Open(nil, iv, cipherWithTag, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gcm open: %w", err)
|
||||||
|
}
|
||||||
|
return plain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeB64(b []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeB64(s string) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
27
internal/utils/helpers.go
Normal file
27
internal/utils/helpers.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorResponse is a simple, reusable error payload.
|
||||||
|
// swagger:model ErrorResponse
|
||||||
|
type ErrorResponse struct {
|
||||||
|
// A machine-readable error code, e.g. "validation_error"
|
||||||
|
// example: validation_error
|
||||||
|
Code string `json:"code"`
|
||||||
|
// Human-readable message
|
||||||
|
// example: slug is required
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteError(w http.ResponseWriter, status int, code, msg string) {
|
||||||
|
WriteJSON(w, status, ErrorResponse{Code: code, Message: msg})
|
||||||
|
}
|
||||||
107
internal/utils/keys.go
Normal file
107
internal/utils/keys.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getMasterKey(db *gorm.DB) ([]byte, error) {
|
||||||
|
var mk models.MasterKey
|
||||||
|
if err := db.Where("is_active = ?", true).Order("created_at DESC").First(&mk).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrNoActiveMasterKey
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("querying master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, err := base64.StdEncoding.DecodeString(mk.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding master key: %w", err)
|
||||||
|
}
|
||||||
|
if len(keyBytes) != 32 {
|
||||||
|
return nil, fmt.Errorf("%w: got %d, want 32", ErrInvalidMasterKeyLen, len(keyBytes))
|
||||||
|
}
|
||||||
|
return keyBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrCreateTenantKey(orgID string, db *gorm.DB) ([]byte, error) {
|
||||||
|
var orgKey models.OrganizationKey
|
||||||
|
err := db.Where("organization_id = ?", orgID).First(&orgKey).Error
|
||||||
|
if err == nil {
|
||||||
|
encKeyB64 := orgKey.EncryptedKey
|
||||||
|
ivB64 := orgKey.IV
|
||||||
|
tagB64 := orgKey.Tag
|
||||||
|
|
||||||
|
encryptedKey, err := DecodeB64(encKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode enc key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
iv, err := DecodeB64(ivB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode iv: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := DecodeB64(tagB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode tag: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
masterKey, err := getMasterKey(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptAESGCM(encryptedKey, masterKey, iv, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tenant key and wrap with the current master key
|
||||||
|
orgUUID, err := uuid.Parse(orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidOrgID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantKey, err := randomBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tenant key gen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
masterKey, err := getMasterKey(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted, iv, tag, err := encryptAESGCM(tenantKey, masterKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap tenant key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mk models.MasterKey
|
||||||
|
if err := db.Where("is_active = ?", true).Order("created_at DESC").First(&mk).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrNoActiveMasterKey
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("querying master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orgKey = models.OrganizationKey{
|
||||||
|
OrganizationID: orgUUID,
|
||||||
|
MasterKeyID: mk.ID,
|
||||||
|
EncryptedKey: EncodeB64(encrypted),
|
||||||
|
IV: EncodeB64(iv),
|
||||||
|
Tag: EncodeB64(tag),
|
||||||
|
}
|
||||||
|
if err := db.Create(&orgKey).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("persist org key: %w", err)
|
||||||
|
}
|
||||||
|
return tenantKey, nil
|
||||||
|
}
|
||||||
44
internal/utils/org-crypto.go
Normal file
44
internal/utils/org-crypto.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncryptForOrg(orgID uuid.UUID, plaintext []byte, db *gorm.DB) (cipherB64, ivB64, tagB64 string, err error) {
|
||||||
|
tenantKey, err := getOrCreateTenantKey(orgID.String(), db)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", err
|
||||||
|
}
|
||||||
|
ct, iv, tag, err := encryptAESGCM(plaintext, tenantKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", err
|
||||||
|
}
|
||||||
|
return EncodeB64(ct), EncodeB64(iv), EncodeB64(tag), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptForOrg(orgID uuid.UUID, cipherB64, ivB64, tagB64 string, db *gorm.DB) (string, error) {
|
||||||
|
tenantKey, err := getOrCreateTenantKey(orgID.String(), db)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ct, err := DecodeB64(cipherB64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decode cipher: %w", err)
|
||||||
|
}
|
||||||
|
iv, err := DecodeB64(ivB64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decode iv: %w", err)
|
||||||
|
}
|
||||||
|
tag, err := DecodeB64(tagB64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decode tag: %w", err)
|
||||||
|
}
|
||||||
|
plain, err := decryptAESGCM(ct, tenantKey, iv, tag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(plain), nil
|
||||||
|
}
|
||||||
36
internal/version/version.go
Normal file
36
internal/version/version.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Commit = "none"
|
||||||
|
Date = "unknown"
|
||||||
|
BuiltBy = "local"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Info() string {
|
||||||
|
v := fmt.Sprintf("Version: %s\nCommit: %s\nBuilt: %s\nBuiltBy: %s\nGo: %s %s/%s",
|
||||||
|
Version, Commit, Date, BuiltBy, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
// Include VCS info from embedded build metadata (if available)
|
||||||
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
for _, s := range bi.Settings {
|
||||||
|
switch s.Key {
|
||||||
|
case "vcs":
|
||||||
|
v += fmt.Sprintf("\nVCS: %s", s.Value)
|
||||||
|
case "vcs.revision":
|
||||||
|
v += fmt.Sprintf("\nRevision: %s", s.Value)
|
||||||
|
case "vcs.time":
|
||||||
|
v += fmt.Sprintf("\nCommitTime: %s", s.Value)
|
||||||
|
case "vcs.modified":
|
||||||
|
v += fmt.Sprintf("\nModified: %s", s.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
16
internal/web/devproxy.go
Normal file
16
internal/web/devproxy.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DevProxy(target string) (http.Handler, error) {
|
||||||
|
u, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := httputil.NewSingleHostReverseProxy(u)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
2
internal/web/dist/assets/index-BvUUUOIq.css
vendored
Normal file
2
internal/web/dist/assets/index-BvUUUOIq.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-BvUUUOIq.css.br
vendored
Normal file
BIN
internal/web/dist/assets/index-BvUUUOIq.css.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-BvUUUOIq.css.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-BvUUUOIq.css.gz
vendored
Normal file
Binary file not shown.
10
internal/web/dist/assets/index-CFwByDWI.js
vendored
Normal file
10
internal/web/dist/assets/index-CFwByDWI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-CFwByDWI.js.br
vendored
Normal file
BIN
internal/web/dist/assets/index-CFwByDWI.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-CFwByDWI.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-CFwByDWI.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/index-CFwByDWI.js.map
vendored
Normal file
1
internal/web/dist/assets/index-CFwByDWI.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
internal/web/dist/assets/react-BZmgNp9X.js
vendored
Normal file
4
internal/web/dist/assets/react-BZmgNp9X.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/react-BZmgNp9X.js.br
vendored
Normal file
BIN
internal/web/dist/assets/react-BZmgNp9X.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/react-BZmgNp9X.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/react-BZmgNp9X.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/react-BZmgNp9X.js.map
vendored
Normal file
1
internal/web/dist/assets/react-BZmgNp9X.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
15
internal/web/dist/index.html
vendored
Normal file
15
internal/web/dist/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ui</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-CFwByDWI.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/react-BZmgNp9X.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-BvUUUOIq.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
internal/web/dist/index.html.br
vendored
Normal file
BIN
internal/web/dist/index.html.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/index.html.gz
vendored
Normal file
BIN
internal/web/dist/index.html.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/vite.svg
vendored
Normal file
1
internal/web/dist/vite.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
239
internal/web/static.go
Normal file
239
internal/web/static.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: Vite outputs to web/dist with assets in dist/assets.
|
||||||
|
// If you add more nested folders in the future, include them here too.
|
||||||
|
|
||||||
|
//go:embed dist
|
||||||
|
var distFS embed.FS
|
||||||
|
|
||||||
|
// spaFileSystem serves embedded dist/ files with SPA fallback to index.html
|
||||||
|
type spaFileSystem struct {
|
||||||
|
fs fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s spaFileSystem) Open(name string) (fs.File, error) {
|
||||||
|
// Normalize, strip leading slash
|
||||||
|
if strings.HasPrefix(name, "/") {
|
||||||
|
name = name[1:]
|
||||||
|
}
|
||||||
|
// Try exact file
|
||||||
|
f, err := s.fs.Open(name)
|
||||||
|
if err == nil {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the requested file doesn't exist, fall back to index.html for SPA routes
|
||||||
|
// BUT only if it's not obviously a static asset extension
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
switch ext {
|
||||||
|
case ".js", ".css", ".map", ".json", ".txt", ".ico", ".png", ".jpg", ".jpeg",
|
||||||
|
".svg", ".webp", ".gif", ".woff", ".woff2", ".ttf", ".otf", ".eot", ".wasm", ".br", ".gz":
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.fs.Open("index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDistFS() (fs.FS, error) {
|
||||||
|
return fs.Sub(distFS, "dist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPAHandler returns an http.Handler that serves the embedded UI (with caching)
|
||||||
|
func SPAHandler() (http.Handler, error) {
|
||||||
|
sub, err := newDistFS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
spa := spaFileSystem{fs: sub}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/api/") ||
|
||||||
|
r.URL.Path == "/api" ||
|
||||||
|
strings.HasPrefix(r.URL.Path, "/swagger") ||
|
||||||
|
strings.HasPrefix(r.URL.Path, "/debug/pprof") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
||||||
|
if filePath == "" {
|
||||||
|
filePath = "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try compressed variants for assets and HTML
|
||||||
|
// NOTE: we only change *Content-Encoding*; Content-Type derives from original ext
|
||||||
|
// Always vary on Accept-Encoding
|
||||||
|
w.Header().Add("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
enc := r.Header.Get("Accept-Encoding")
|
||||||
|
if tryServeCompressed(w, r, spa, filePath, enc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: normal open (or SPA fallback)
|
||||||
|
f, err := spa.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if strings.HasSuffix(filePath, ".html") {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, _ := f.Stat()
|
||||||
|
modTime := time.Now()
|
||||||
|
if info != nil {
|
||||||
|
modTime = info.ModTime()
|
||||||
|
}
|
||||||
|
http.ServeContent(w, r, filePath, modTime, file{f})
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryServeCompressed(w http.ResponseWriter, r *http.Request, spa spaFileSystem, filePath, enc string) bool {
|
||||||
|
wantsBR := strings.Contains(enc, "br")
|
||||||
|
wantsGZ := strings.Contains(enc, "gzip")
|
||||||
|
|
||||||
|
type cand struct {
|
||||||
|
logical string // MIME/type decision uses this (uncompressed name)
|
||||||
|
physical string // actual file we open (with .br/.gz)
|
||||||
|
enc string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cands []cand
|
||||||
|
|
||||||
|
// 1) direct compressed variant of requested path (rare for SPA routes, but cheap to try)
|
||||||
|
if wantsBR {
|
||||||
|
cands = append(cands, cand{logical: filePath, physical: filePath + ".br", enc: "br"})
|
||||||
|
}
|
||||||
|
if wantsGZ {
|
||||||
|
cands = append(cands, cand{logical: filePath, physical: filePath + ".gz", enc: "gzip"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) SPA route: fall back to compressed index.html
|
||||||
|
if filepath.Ext(filePath) == "" {
|
||||||
|
if wantsBR {
|
||||||
|
cands = append(cands, cand{logical: "index.html", physical: "index.html.br", enc: "br"})
|
||||||
|
}
|
||||||
|
if wantsGZ {
|
||||||
|
cands = append(cands, cand{logical: "index.html", physical: "index.html.gz", enc: "gzip"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cands {
|
||||||
|
f, err := spa.fs.Open(c.physical) // open EXACT path so we don't accidentally get SPA fallback
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Cache headers
|
||||||
|
if strings.HasSuffix(c.logical, ".html") {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ct := mimeByExt(path.Ext(c.logical)); ct != "" {
|
||||||
|
w.Header().Set("Content-Type", ct)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", c.enc)
|
||||||
|
w.Header().Add("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
info, _ := f.Stat()
|
||||||
|
modTime := time.Now()
|
||||||
|
if info != nil {
|
||||||
|
modTime = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the precompressed bytes
|
||||||
|
http.ServeContent(w, r, c.physical, modTime, file{f})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIfExists(w http.ResponseWriter, r *http.Request, spa spaFileSystem, filePath, ext, encoding string) bool {
|
||||||
|
cf := filePath + ext
|
||||||
|
f, err := spa.Open(cf)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Set caching headers
|
||||||
|
if strings.HasSuffix(filePath, ".html") {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
}
|
||||||
|
// Preserve original content type by extension of *uncompressed* file
|
||||||
|
if ct := mimeByExt(path.Ext(filePath)); ct != "" {
|
||||||
|
w.Header().Set("Content-Type", ct)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", encoding)
|
||||||
|
|
||||||
|
info, _ := f.Stat()
|
||||||
|
modTime := time.Now()
|
||||||
|
if info != nil {
|
||||||
|
modTime = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the compressed bytes as an io.ReadSeeker if possible
|
||||||
|
http.ServeContent(w, r, cf, modTime, file{f})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func mimeByExt(ext string) string {
|
||||||
|
switch strings.ToLower(ext) {
|
||||||
|
case ".html":
|
||||||
|
return "text/html; charset=utf-8"
|
||||||
|
case ".js":
|
||||||
|
return "application/javascript"
|
||||||
|
case ".css":
|
||||||
|
return "text/css; charset=utf-8"
|
||||||
|
case ".json":
|
||||||
|
return "application/json"
|
||||||
|
case ".svg":
|
||||||
|
return "image/svg+xml"
|
||||||
|
case ".png":
|
||||||
|
return "image/png"
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return "image/jpeg"
|
||||||
|
case ".webp":
|
||||||
|
return "image/webp"
|
||||||
|
case ".ico":
|
||||||
|
return "image/x-icon"
|
||||||
|
case ".woff2":
|
||||||
|
return "font/woff2"
|
||||||
|
case ".woff":
|
||||||
|
return "font/woff"
|
||||||
|
default:
|
||||||
|
return "" // let Go sniff if empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// file wraps fs.File to implement io.ReadSeeker if possible (for ServeContent)
|
||||||
|
type file struct{ fs.File }
|
||||||
|
|
||||||
|
func (f file) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if s, ok := f.File.(io.Seeker); ok {
|
||||||
|
return s.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
// Fallback: not seekable
|
||||||
|
return 0, fs.ErrInvalid
|
||||||
|
}
|
||||||
37
main.go
Normal file
37
main.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/glueops/autoglue/cmd"
|
||||||
|
|
||||||
|
// @title AutoGlue API
|
||||||
|
// @version 1.0
|
||||||
|
// @description API for managing K3s clusters across cloud providers
|
||||||
|
|
||||||
|
// @contact.name GlueOps
|
||||||
|
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @schemes http https
|
||||||
|
// @host localhost:8080
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey BearerAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @description Bearer token authentication
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
|
// @in header
|
||||||
|
// @name X-API-KEY
|
||||||
|
// @description User API key
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey OrgKeyAuth
|
||||||
|
// @in header
|
||||||
|
// @name X-ORG-KEY
|
||||||
|
// @description Org-level key/secret authentication
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey OrgSecretAuth
|
||||||
|
// @in header
|
||||||
|
// @name X-ORG-SECRET
|
||||||
|
// @description Org-level secret
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
7
openapitools.json
Normal file
7
openapitools.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
|
"spaces": 2,
|
||||||
|
"generator-cli": {
|
||||||
|
"version": "7.17.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
postgres/Dockerfile
Normal file
10
postgres/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM postgres:latest@sha256:feff5b24fedd610975a1f5e743c51a4b360437f4dc3a11acf740dcd708f413f6
|
||||||
|
|
||||||
|
RUN cd /var/lib/postgresql/ && \
|
||||||
|
openssl req -new -text -passout pass:abcd -subj /CN=localhost -out server.req -keyout privkey.pem && \
|
||||||
|
openssl rsa -in privkey.pem -passin pass:abcd -out server.key && \
|
||||||
|
openssl req -x509 -in server.req -text -key server.key -out server.crt && \
|
||||||
|
chmod 600 server.key && \
|
||||||
|
chown postgres:postgres server.key
|
||||||
|
|
||||||
|
CMD ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/var/lib/postgresql/server.crt", "-c", "ssl_key_file=/var/lib/postgresql/server.key" ]
|
||||||
24
sdk/go/.gitignore
vendored
Normal file
24
sdk/go/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
23
sdk/go/.openapi-generator-ignore
Normal file
23
sdk/go/.openapi-generator-ignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# OpenAPI Generator Ignore
|
||||||
|
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||||
|
|
||||||
|
# Use this file to prevent files from being overwritten by the generator.
|
||||||
|
# The patterns follow closely to .gitignore or .dockerignore.
|
||||||
|
|
||||||
|
# As an example, the C# client generator defines ApiClient.cs.
|
||||||
|
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||||
|
#ApiClient.cs
|
||||||
|
|
||||||
|
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||||
|
#foo/*/qux
|
||||||
|
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||||
|
#foo/**/qux
|
||||||
|
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can also negate patterns with an exclamation (!).
|
||||||
|
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||||
|
#docs/*.md
|
||||||
|
# Then explicitly reverse the ignore rule for a single file:
|
||||||
|
#!docs/README.md
|
||||||
86
sdk/go/.openapi-generator/FILES
Normal file
86
sdk/go/.openapi-generator/FILES
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
.gitignore
|
||||||
|
.travis.yml
|
||||||
|
README.md
|
||||||
|
api/openapi.yaml
|
||||||
|
api_auth.go
|
||||||
|
api_me.go
|
||||||
|
api_me_api_keys.go
|
||||||
|
api_orgs.go
|
||||||
|
api_servers.go
|
||||||
|
api_ssh.go
|
||||||
|
api_taints.go
|
||||||
|
client.go
|
||||||
|
configuration.go
|
||||||
|
docs/AuthAPI.md
|
||||||
|
docs/DtoAuthStartResponse.md
|
||||||
|
docs/DtoCreateSSHRequest.md
|
||||||
|
docs/DtoCreateServerRequest.md
|
||||||
|
docs/DtoCreateTaintRequest.md
|
||||||
|
docs/DtoJWK.md
|
||||||
|
docs/DtoJWKS.md
|
||||||
|
docs/DtoLogoutRequest.md
|
||||||
|
docs/DtoRefreshRequest.md
|
||||||
|
docs/DtoServerResponse.md
|
||||||
|
docs/DtoSshResponse.md
|
||||||
|
docs/DtoSshRevealResponse.md
|
||||||
|
docs/DtoTaintResponse.md
|
||||||
|
docs/DtoTokenPair.md
|
||||||
|
docs/DtoUpdateServerRequest.md
|
||||||
|
docs/DtoUpdateTaintRequest.md
|
||||||
|
docs/HandlersCreateUserKeyRequest.md
|
||||||
|
docs/HandlersMeResponse.md
|
||||||
|
docs/HandlersMemberOut.md
|
||||||
|
docs/HandlersMemberUpsertReq.md
|
||||||
|
docs/HandlersOrgCreateReq.md
|
||||||
|
docs/HandlersOrgKeyCreateReq.md
|
||||||
|
docs/HandlersOrgKeyCreateResp.md
|
||||||
|
docs/HandlersOrgUpdateReq.md
|
||||||
|
docs/HandlersUpdateMeRequest.md
|
||||||
|
docs/HandlersUserAPIKeyOut.md
|
||||||
|
docs/MeAPI.md
|
||||||
|
docs/MeAPIKeysAPI.md
|
||||||
|
docs/ModelsAPIKey.md
|
||||||
|
docs/ModelsOrganization.md
|
||||||
|
docs/ModelsUser.md
|
||||||
|
docs/ModelsUserEmail.md
|
||||||
|
docs/OrgsAPI.md
|
||||||
|
docs/ServersAPI.md
|
||||||
|
docs/SshAPI.md
|
||||||
|
docs/TaintsAPI.md
|
||||||
|
docs/UtilsErrorResponse.md
|
||||||
|
git_push.sh
|
||||||
|
go.mod
|
||||||
|
go.sum
|
||||||
|
model_dto_auth_start_response.go
|
||||||
|
model_dto_create_server_request.go
|
||||||
|
model_dto_create_ssh_request.go
|
||||||
|
model_dto_create_taint_request.go
|
||||||
|
model_dto_jwk.go
|
||||||
|
model_dto_jwks.go
|
||||||
|
model_dto_logout_request.go
|
||||||
|
model_dto_refresh_request.go
|
||||||
|
model_dto_server_response.go
|
||||||
|
model_dto_ssh_response.go
|
||||||
|
model_dto_ssh_reveal_response.go
|
||||||
|
model_dto_taint_response.go
|
||||||
|
model_dto_token_pair.go
|
||||||
|
model_dto_update_server_request.go
|
||||||
|
model_dto_update_taint_request.go
|
||||||
|
model_handlers_create_user_key_request.go
|
||||||
|
model_handlers_me_response.go
|
||||||
|
model_handlers_member_out.go
|
||||||
|
model_handlers_member_upsert_req.go
|
||||||
|
model_handlers_org_create_req.go
|
||||||
|
model_handlers_org_key_create_req.go
|
||||||
|
model_handlers_org_key_create_resp.go
|
||||||
|
model_handlers_org_update_req.go
|
||||||
|
model_handlers_update_me_request.go
|
||||||
|
model_handlers_user_api_key_out.go
|
||||||
|
model_models_api_key.go
|
||||||
|
model_models_organization.go
|
||||||
|
model_models_user.go
|
||||||
|
model_models_user_email.go
|
||||||
|
model_utils_error_response.go
|
||||||
|
response.go
|
||||||
|
test/api_taints_test.go
|
||||||
|
utils.go
|
||||||
1
sdk/go/.openapi-generator/VERSION
Normal file
1
sdk/go/.openapi-generator/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7.17.0
|
||||||
8
sdk/go/.travis.yml
Normal file
8
sdk/go/.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -d -v .
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go build -v ./
|
||||||
|
|
||||||
261
sdk/go/README.md
Normal file
261
sdk/go/README.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# Go API client for autoglue
|
||||||
|
|
||||||
|
API for managing K3s clusters across cloud providers
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.
|
||||||
|
|
||||||
|
- API version: 1.0
|
||||||
|
- Package version: 1.0.0
|
||||||
|
- Generator version: 7.17.0
|
||||||
|
- Build package: org.openapitools.codegen.languages.GoClientCodegen
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the following dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/stretchr/testify/assert
|
||||||
|
go get golang.org/x/net/context
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the package under your project folder and add the following in import:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import autoglue "github.com/glueops/autoglue-sdk"
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a proxy, set the environment variable `HTTP_PROXY`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration of Server URL
|
||||||
|
|
||||||
|
Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification.
|
||||||
|
|
||||||
|
### Select Server Configuration
|
||||||
|
|
||||||
|
For using other server than the one defined on index 0 set context value `autoglue.ContextServerIndex` of type `int`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := context.WithValue(context.Background(), autoglue.ContextServerIndex, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templated Server URL
|
||||||
|
|
||||||
|
Templated server URL is formatted using default variables from configuration or from context value `autoglue.ContextServerVariables` of type `map[string]string`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := context.WithValue(context.Background(), autoglue.ContextServerVariables, map[string]string{
|
||||||
|
"basePath": "v2",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note, enum values are always validated and all unused variables are silently ignored.
|
||||||
|
|
||||||
|
### URLs Configuration per Operation
|
||||||
|
|
||||||
|
Each operation can use different server URL defined using `OperationServers` map in the `Configuration`.
|
||||||
|
An operation is uniquely identified by `"{classname}Service.{nickname}"` string.
|
||||||
|
Similar rules for overriding default operation server index and variables applies by using `autoglue.ContextOperationServerIndices` and `autoglue.ContextOperationServerVariables` context maps.
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := context.WithValue(context.Background(), autoglue.ContextOperationServerIndices, map[string]int{
|
||||||
|
"{classname}Service.{nickname}": 2,
|
||||||
|
})
|
||||||
|
ctx = context.WithValue(context.Background(), autoglue.ContextOperationServerVariables, map[string]map[string]string{
|
||||||
|
"{classname}Service.{nickname}": {
|
||||||
|
"port": "8443",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation for API Endpoints
|
||||||
|
|
||||||
|
All URIs are relative to *http://localhost:8080/api/v1*
|
||||||
|
|
||||||
|
Class | Method | HTTP request | Description
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
*AuthAPI* | [**AuthCallback**](docs/AuthAPI.md#authcallback) | **Get** /auth/{provider}/callback | Handle social login callback
|
||||||
|
*AuthAPI* | [**AuthStart**](docs/AuthAPI.md#authstart) | **Post** /auth/{provider}/start | Begin social login
|
||||||
|
*AuthAPI* | [**GetJWKS**](docs/AuthAPI.md#getjwks) | **Get** /.well-known/jwks.json | Get JWKS
|
||||||
|
*AuthAPI* | [**Logout**](docs/AuthAPI.md#logout) | **Post** /auth/logout | Revoke refresh token family (logout everywhere)
|
||||||
|
*AuthAPI* | [**Refresh**](docs/AuthAPI.md#refresh) | **Post** /auth/refresh | Rotate refresh token
|
||||||
|
*MeAPI* | [**GetMe**](docs/MeAPI.md#getme) | **Get** /me | Get current user profile
|
||||||
|
*MeAPI* | [**UpdateMe**](docs/MeAPI.md#updateme) | **Patch** /me | Update current user profile
|
||||||
|
*MeAPIKeysAPI* | [**CreateUserAPIKey**](docs/MeAPIKeysAPI.md#createuserapikey) | **Post** /me/api-keys | Create a new user API key
|
||||||
|
*MeAPIKeysAPI* | [**DeleteUserAPIKey**](docs/MeAPIKeysAPI.md#deleteuserapikey) | **Delete** /me/api-keys/{id} | Delete a user API key
|
||||||
|
*MeAPIKeysAPI* | [**ListUserAPIKeys**](docs/MeAPIKeysAPI.md#listuserapikeys) | **Get** /me/api-keys | List my API keys
|
||||||
|
*OrgsAPI* | [**AddOrUpdateMember**](docs/OrgsAPI.md#addorupdatemember) | **Post** /orgs/{id}/members | Add or update a member (owner/admin)
|
||||||
|
*OrgsAPI* | [**CreateOrg**](docs/OrgsAPI.md#createorg) | **Post** /orgs | Create organization
|
||||||
|
*OrgsAPI* | [**CreateOrgKey**](docs/OrgsAPI.md#createorgkey) | **Post** /orgs/{id}/api-keys | Create org key/secret pair (owner/admin)
|
||||||
|
*OrgsAPI* | [**DeleteOrg**](docs/OrgsAPI.md#deleteorg) | **Delete** /orgs/{id} | Delete organization (owner)
|
||||||
|
*OrgsAPI* | [**DeleteOrgKey**](docs/OrgsAPI.md#deleteorgkey) | **Delete** /orgs/{id}/api-keys/{key_id} | Delete org key (owner/admin)
|
||||||
|
*OrgsAPI* | [**GetOrg**](docs/OrgsAPI.md#getorg) | **Get** /orgs/{id} | Get organization
|
||||||
|
*OrgsAPI* | [**ListMembers**](docs/OrgsAPI.md#listmembers) | **Get** /orgs/{id}/members | List members in org
|
||||||
|
*OrgsAPI* | [**ListMyOrgs**](docs/OrgsAPI.md#listmyorgs) | **Get** /orgs | List organizations I belong to
|
||||||
|
*OrgsAPI* | [**ListOrgKeys**](docs/OrgsAPI.md#listorgkeys) | **Get** /orgs/{id}/api-keys | List org-scoped API keys (no secrets)
|
||||||
|
*OrgsAPI* | [**RemoveMember**](docs/OrgsAPI.md#removemember) | **Delete** /orgs/{id}/members/{user_id} | Remove a member (owner/admin)
|
||||||
|
*OrgsAPI* | [**UpdateOrg**](docs/OrgsAPI.md#updateorg) | **Patch** /orgs/{id} | Update organization (owner/admin)
|
||||||
|
*ServersAPI* | [**CreateServer**](docs/ServersAPI.md#createserver) | **Post** /servers | Create server (org scoped)
|
||||||
|
*ServersAPI* | [**DeleteServer**](docs/ServersAPI.md#deleteserver) | **Delete** /servers/{id} | Delete server (org scoped)
|
||||||
|
*ServersAPI* | [**GetServer**](docs/ServersAPI.md#getserver) | **Get** /servers/{id} | Get server by ID (org scoped)
|
||||||
|
*ServersAPI* | [**ListServers**](docs/ServersAPI.md#listservers) | **Get** /servers | List servers (org scoped)
|
||||||
|
*ServersAPI* | [**UpdateServer**](docs/ServersAPI.md#updateserver) | **Patch** /servers/{id} | Update server (org scoped)
|
||||||
|
*SshAPI* | [**CreateSSHKey**](docs/SshAPI.md#createsshkey) | **Post** /ssh | Create ssh keypair (org scoped)
|
||||||
|
*SshAPI* | [**DeleteSSHKey**](docs/SshAPI.md#deletesshkey) | **Delete** /ssh/{id} | Delete ssh keypair (org scoped)
|
||||||
|
*SshAPI* | [**DownloadSSHKey**](docs/SshAPI.md#downloadsshkey) | **Get** /ssh/{id}/download | Download ssh key files by ID (org scoped)
|
||||||
|
*SshAPI* | [**GetSSHKey**](docs/SshAPI.md#getsshkey) | **Get** /ssh/{id} | Get ssh key by ID (org scoped)
|
||||||
|
*SshAPI* | [**ListPublicSshKeys**](docs/SshAPI.md#listpublicsshkeys) | **Get** /ssh | List ssh keys (org scoped)
|
||||||
|
*TaintsAPI* | [**CreateTaint**](docs/TaintsAPI.md#createtaint) | **Post** /taints | Create node taint (org scoped)
|
||||||
|
*TaintsAPI* | [**DeleteTaint**](docs/TaintsAPI.md#deletetaint) | **Delete** /taints/{id} | Delete taint (org scoped)
|
||||||
|
*TaintsAPI* | [**GetTaint**](docs/TaintsAPI.md#gettaint) | **Get** /taints/{id} | Get node taint by ID (org scoped)
|
||||||
|
*TaintsAPI* | [**ListTaints**](docs/TaintsAPI.md#listtaints) | **Get** /taints | List node pool taints (org scoped)
|
||||||
|
*TaintsAPI* | [**UpdateTaint**](docs/TaintsAPI.md#updatetaint) | **Patch** /taints/{id} | Update node taint (org scoped)
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation For Models
|
||||||
|
|
||||||
|
- [DtoAuthStartResponse](docs/DtoAuthStartResponse.md)
|
||||||
|
- [DtoCreateSSHRequest](docs/DtoCreateSSHRequest.md)
|
||||||
|
- [DtoCreateServerRequest](docs/DtoCreateServerRequest.md)
|
||||||
|
- [DtoCreateTaintRequest](docs/DtoCreateTaintRequest.md)
|
||||||
|
- [DtoJWK](docs/DtoJWK.md)
|
||||||
|
- [DtoJWKS](docs/DtoJWKS.md)
|
||||||
|
- [DtoLogoutRequest](docs/DtoLogoutRequest.md)
|
||||||
|
- [DtoRefreshRequest](docs/DtoRefreshRequest.md)
|
||||||
|
- [DtoServerResponse](docs/DtoServerResponse.md)
|
||||||
|
- [DtoSshResponse](docs/DtoSshResponse.md)
|
||||||
|
- [DtoSshRevealResponse](docs/DtoSshRevealResponse.md)
|
||||||
|
- [DtoTaintResponse](docs/DtoTaintResponse.md)
|
||||||
|
- [DtoTokenPair](docs/DtoTokenPair.md)
|
||||||
|
- [DtoUpdateServerRequest](docs/DtoUpdateServerRequest.md)
|
||||||
|
- [DtoUpdateTaintRequest](docs/DtoUpdateTaintRequest.md)
|
||||||
|
- [HandlersCreateUserKeyRequest](docs/HandlersCreateUserKeyRequest.md)
|
||||||
|
- [HandlersMeResponse](docs/HandlersMeResponse.md)
|
||||||
|
- [HandlersMemberOut](docs/HandlersMemberOut.md)
|
||||||
|
- [HandlersMemberUpsertReq](docs/HandlersMemberUpsertReq.md)
|
||||||
|
- [HandlersOrgCreateReq](docs/HandlersOrgCreateReq.md)
|
||||||
|
- [HandlersOrgKeyCreateReq](docs/HandlersOrgKeyCreateReq.md)
|
||||||
|
- [HandlersOrgKeyCreateResp](docs/HandlersOrgKeyCreateResp.md)
|
||||||
|
- [HandlersOrgUpdateReq](docs/HandlersOrgUpdateReq.md)
|
||||||
|
- [HandlersUpdateMeRequest](docs/HandlersUpdateMeRequest.md)
|
||||||
|
- [HandlersUserAPIKeyOut](docs/HandlersUserAPIKeyOut.md)
|
||||||
|
- [ModelsAPIKey](docs/ModelsAPIKey.md)
|
||||||
|
- [ModelsOrganization](docs/ModelsOrganization.md)
|
||||||
|
- [ModelsUser](docs/ModelsUser.md)
|
||||||
|
- [ModelsUserEmail](docs/ModelsUserEmail.md)
|
||||||
|
- [UtilsErrorResponse](docs/UtilsErrorResponse.md)
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation For Authorization
|
||||||
|
|
||||||
|
|
||||||
|
Authentication schemes defined for the API:
|
||||||
|
### ApiKeyAuth
|
||||||
|
|
||||||
|
- **Type**: API key
|
||||||
|
- **API key parameter name**: X-API-KEY
|
||||||
|
- **Location**: HTTP header
|
||||||
|
|
||||||
|
Note, each API key must be added to a map of `map[string]APIKey` where the key is: ApiKeyAuth and passed in as the auth context for each request.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
autoglue.ContextAPIKeys,
|
||||||
|
map[string]autoglue.APIKey{
|
||||||
|
"ApiKeyAuth": {Key: "API_KEY_STRING"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r, err := client.Service.Operation(auth, args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### BearerAuth
|
||||||
|
|
||||||
|
- **Type**: API key
|
||||||
|
- **API key parameter name**: Authorization
|
||||||
|
- **Location**: HTTP header
|
||||||
|
|
||||||
|
Note, each API key must be added to a map of `map[string]APIKey` where the key is: BearerAuth and passed in as the auth context for each request.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
autoglue.ContextAPIKeys,
|
||||||
|
map[string]autoglue.APIKey{
|
||||||
|
"BearerAuth": {Key: "API_KEY_STRING"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r, err := client.Service.Operation(auth, args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrgKeyAuth
|
||||||
|
|
||||||
|
- **Type**: API key
|
||||||
|
- **API key parameter name**: X-ORG-KEY
|
||||||
|
- **Location**: HTTP header
|
||||||
|
|
||||||
|
Note, each API key must be added to a map of `map[string]APIKey` where the key is: OrgKeyAuth and passed in as the auth context for each request.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
autoglue.ContextAPIKeys,
|
||||||
|
map[string]autoglue.APIKey{
|
||||||
|
"OrgKeyAuth": {Key: "API_KEY_STRING"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r, err := client.Service.Operation(auth, args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrgSecretAuth
|
||||||
|
|
||||||
|
- **Type**: API key
|
||||||
|
- **API key parameter name**: X-ORG-SECRET
|
||||||
|
- **Location**: HTTP header
|
||||||
|
|
||||||
|
Note, each API key must be added to a map of `map[string]APIKey` where the key is: OrgSecretAuth and passed in as the auth context for each request.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
autoglue.ContextAPIKeys,
|
||||||
|
map[string]autoglue.APIKey{
|
||||||
|
"OrgSecretAuth": {Key: "API_KEY_STRING"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r, err := client.Service.Operation(auth, args)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation for Utility Methods
|
||||||
|
|
||||||
|
Due to the fact that model structure members are all pointers, this package contains
|
||||||
|
a number of utility functions to easily obtain pointers to values of basic types.
|
||||||
|
Each of these functions takes a value of the given basic type and returns a pointer to it:
|
||||||
|
|
||||||
|
* `PtrBool`
|
||||||
|
* `PtrInt`
|
||||||
|
* `PtrInt32`
|
||||||
|
* `PtrInt64`
|
||||||
|
* `PtrFloat`
|
||||||
|
* `PtrFloat32`
|
||||||
|
* `PtrFloat64`
|
||||||
|
* `PtrString`
|
||||||
|
* `PtrTime`
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2086
sdk/go/api/openapi.yaml
Normal file
2086
sdk/go/api/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
537
sdk/go/api_auth.go
Normal file
537
sdk/go/api_auth.go
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
/*
|
||||||
|
AutoGlue API
|
||||||
|
|
||||||
|
API for managing K3s clusters across cloud providers
|
||||||
|
|
||||||
|
API version: 1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package autoglue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthAPIService AuthAPI service
|
||||||
|
type AuthAPIService service
|
||||||
|
|
||||||
|
type ApiAuthCallbackRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *AuthAPIService
|
||||||
|
provider string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiAuthCallbackRequest) Execute() (*DtoTokenPair, *http.Response, error) {
|
||||||
|
return r.ApiService.AuthCallbackExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
AuthCallback Handle social login callback
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@param provider google|github
|
||||||
|
@return ApiAuthCallbackRequest
|
||||||
|
*/
|
||||||
|
func (a *AuthAPIService) AuthCallback(ctx context.Context, provider string) ApiAuthCallbackRequest {
|
||||||
|
return ApiAuthCallbackRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
provider: provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return DtoTokenPair
|
||||||
|
func (a *AuthAPIService) AuthCallbackExecute(r ApiAuthCallbackRequest) (*DtoTokenPair, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *DtoTokenPair
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AuthAPIService.AuthCallback")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/auth/{provider}/callback"
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"provider"+"}", url.PathEscape(parameterValueToString(r.provider, "provider")), -1)
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiAuthStartRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *AuthAPIService
|
||||||
|
provider string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiAuthStartRequest) Execute() (*DtoAuthStartResponse, *http.Response, error) {
|
||||||
|
return r.ApiService.AuthStartExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
AuthStart Begin social login
|
||||||
|
|
||||||
|
Returns provider authorization URL for the frontend to redirect
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@param provider google|github
|
||||||
|
@return ApiAuthStartRequest
|
||||||
|
*/
|
||||||
|
func (a *AuthAPIService) AuthStart(ctx context.Context, provider string) ApiAuthStartRequest {
|
||||||
|
return ApiAuthStartRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
provider: provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return DtoAuthStartResponse
|
||||||
|
func (a *AuthAPIService) AuthStartExecute(r ApiAuthStartRequest) (*DtoAuthStartResponse, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodPost
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *DtoAuthStartResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AuthAPIService.AuthStart")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/auth/{provider}/start"
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"provider"+"}", url.PathEscape(parameterValueToString(r.provider, "provider")), -1)
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiGetJWKSRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *AuthAPIService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetJWKSRequest) Execute() (*DtoJWKS, *http.Response, error) {
|
||||||
|
return r.ApiService.GetJWKSExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetJWKS Get JWKS
|
||||||
|
|
||||||
|
Returns the JSON Web Key Set for token verification
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiGetJWKSRequest
|
||||||
|
*/
|
||||||
|
func (a *AuthAPIService) GetJWKS(ctx context.Context) ApiGetJWKSRequest {
|
||||||
|
return ApiGetJWKSRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return DtoJWKS
|
||||||
|
func (a *AuthAPIService) GetJWKSExecute(r ApiGetJWKSRequest) (*DtoJWKS, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *DtoJWKS
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AuthAPIService.GetJWKS")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/.well-known/jwks.json"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiLogoutRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *AuthAPIService
|
||||||
|
body *DtoLogoutRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token
|
||||||
|
func (r ApiLogoutRequest) Body(body DtoLogoutRequest) ApiLogoutRequest {
|
||||||
|
r.body = &body
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiLogoutRequest) Execute() (*http.Response, error) {
|
||||||
|
return r.ApiService.LogoutExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Logout Revoke refresh token family (logout everywhere)
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiLogoutRequest
|
||||||
|
*/
|
||||||
|
func (a *AuthAPIService) Logout(ctx context.Context) ApiLogoutRequest {
|
||||||
|
return ApiLogoutRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
func (a *AuthAPIService) LogoutExecute(r ApiLogoutRequest) (*http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodPost
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AuthAPIService.Logout")
|
||||||
|
if err != nil {
|
||||||
|
return nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/auth/logout"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
if r.body == nil {
|
||||||
|
return nil, reportError("body is required and must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
// body params
|
||||||
|
localVarPostBody = r.body
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiRefreshRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *AuthAPIService
|
||||||
|
body *DtoRefreshRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token
|
||||||
|
func (r ApiRefreshRequest) Body(body DtoRefreshRequest) ApiRefreshRequest {
|
||||||
|
r.body = &body
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiRefreshRequest) Execute() (*DtoTokenPair, *http.Response, error) {
|
||||||
|
return r.ApiService.RefreshExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Refresh Rotate refresh token
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiRefreshRequest
|
||||||
|
*/
|
||||||
|
func (a *AuthAPIService) Refresh(ctx context.Context) ApiRefreshRequest {
|
||||||
|
return ApiRefreshRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return DtoTokenPair
|
||||||
|
func (a *AuthAPIService) RefreshExecute(r ApiRefreshRequest) (*DtoTokenPair, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodPost
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *DtoTokenPair
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AuthAPIService.Refresh")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/auth/refresh"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
if r.body == nil {
|
||||||
|
return localVarReturnValue, nil, reportError("body is required and must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
// body params
|
||||||
|
localVarPostBody = r.body
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
286
sdk/go/api_me.go
Normal file
286
sdk/go/api_me.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
AutoGlue API
|
||||||
|
|
||||||
|
API for managing K3s clusters across cloud providers
|
||||||
|
|
||||||
|
API version: 1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package autoglue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MeAPIService MeAPI service
|
||||||
|
type MeAPIService service
|
||||||
|
|
||||||
|
type ApiGetMeRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MeAPIService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetMeRequest) Execute() (*HandlersMeResponse, *http.Response, error) {
|
||||||
|
return r.ApiService.GetMeExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetMe Get current user profile
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiGetMeRequest
|
||||||
|
*/
|
||||||
|
func (a *MeAPIService) GetMe(ctx context.Context) ApiGetMeRequest {
|
||||||
|
return ApiGetMeRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return HandlersMeResponse
|
||||||
|
func (a *MeAPIService) GetMeExecute(r ApiGetMeRequest) (*HandlersMeResponse, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *HandlersMeResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeAPIService.GetMe")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/me"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["ApiKeyAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["X-API-KEY"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["BearerAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["Authorization"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiUpdateMeRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MeAPIService
|
||||||
|
body *HandlersUpdateMeRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch profile
|
||||||
|
func (r ApiUpdateMeRequest) Body(body HandlersUpdateMeRequest) ApiUpdateMeRequest {
|
||||||
|
r.body = &body
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiUpdateMeRequest) Execute() (*ModelsUser, *http.Response, error) {
|
||||||
|
return r.ApiService.UpdateMeExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
UpdateMe Update current user profile
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiUpdateMeRequest
|
||||||
|
*/
|
||||||
|
func (a *MeAPIService) UpdateMe(ctx context.Context) ApiUpdateMeRequest {
|
||||||
|
return ApiUpdateMeRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return ModelsUser
|
||||||
|
func (a *MeAPIService) UpdateMeExecute(r ApiUpdateMeRequest) (*ModelsUser, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodPatch
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *ModelsUser
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeAPIService.UpdateMe")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/me"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
if r.body == nil {
|
||||||
|
return localVarReturnValue, nil, reportError("body is required and must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
// body params
|
||||||
|
localVarPostBody = r.body
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["ApiKeyAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["X-API-KEY"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["BearerAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["Authorization"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
393
sdk/go/api_me_api_keys.go
Normal file
393
sdk/go/api_me_api_keys.go
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
AutoGlue API
|
||||||
|
|
||||||
|
API for managing K3s clusters across cloud providers
|
||||||
|
|
||||||
|
API version: 1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package autoglue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MeAPIKeysAPIService MeAPIKeysAPI service
|
||||||
|
type MeAPIKeysAPIService service
|
||||||
|
|
||||||
|
type ApiCreateUserAPIKeyRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MeAPIKeysAPIService
|
||||||
|
body *HandlersCreateUserKeyRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key options
|
||||||
|
func (r ApiCreateUserAPIKeyRequest) Body(body HandlersCreateUserKeyRequest) ApiCreateUserAPIKeyRequest {
|
||||||
|
r.body = &body
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiCreateUserAPIKeyRequest) Execute() (*HandlersUserAPIKeyOut, *http.Response, error) {
|
||||||
|
return r.ApiService.CreateUserAPIKeyExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CreateUserAPIKey Create a new user API key
|
||||||
|
|
||||||
|
Returns the plaintext key once. Store it securely on the client side.
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiCreateUserAPIKeyRequest
|
||||||
|
*/
|
||||||
|
func (a *MeAPIKeysAPIService) CreateUserAPIKey(ctx context.Context) ApiCreateUserAPIKeyRequest {
|
||||||
|
return ApiCreateUserAPIKeyRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return HandlersUserAPIKeyOut
|
||||||
|
func (a *MeAPIKeysAPIService) CreateUserAPIKeyExecute(r ApiCreateUserAPIKeyRequest) (*HandlersUserAPIKeyOut, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodPost
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue *HandlersUserAPIKeyOut
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeAPIKeysAPIService.CreateUserAPIKey")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/me/api-keys"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
if r.body == nil {
|
||||||
|
return localVarReturnValue, nil, reportError("body is required and must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
// body params
|
||||||
|
localVarPostBody = r.body
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["ApiKeyAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["X-API-KEY"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["BearerAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["Authorization"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiDeleteUserAPIKeyRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MeAPIKeysAPIService
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiDeleteUserAPIKeyRequest) Execute() (*http.Response, error) {
|
||||||
|
return r.ApiService.DeleteUserAPIKeyExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DeleteUserAPIKey Delete a user API key
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@param id Key ID (UUID)
|
||||||
|
@return ApiDeleteUserAPIKeyRequest
|
||||||
|
*/
|
||||||
|
func (a *MeAPIKeysAPIService) DeleteUserAPIKey(ctx context.Context, id string) ApiDeleteUserAPIKeyRequest {
|
||||||
|
return ApiDeleteUserAPIKeyRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
id: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
func (a *MeAPIKeysAPIService) DeleteUserAPIKeyExecute(r ApiDeleteUserAPIKeyRequest) (*http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodDelete
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeAPIKeysAPIService.DeleteUserAPIKey")
|
||||||
|
if err != nil {
|
||||||
|
return nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/me/api-keys/{id}"
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1)
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["BearerAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["Authorization"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiListUserAPIKeysRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MeAPIKeysAPIService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiListUserAPIKeysRequest) Execute() ([]HandlersUserAPIKeyOut, *http.Response, error) {
|
||||||
|
return r.ApiService.ListUserAPIKeysExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListUserAPIKeys List my API keys
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@return ApiListUserAPIKeysRequest
|
||||||
|
*/
|
||||||
|
func (a *MeAPIKeysAPIService) ListUserAPIKeys(ctx context.Context) ApiListUserAPIKeysRequest {
|
||||||
|
return ApiListUserAPIKeysRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
//
|
||||||
|
// @return []HandlersUserAPIKeyOut
|
||||||
|
func (a *MeAPIKeysAPIService) ListUserAPIKeysExecute(r ApiListUserAPIKeysRequest) ([]HandlersUserAPIKeyOut, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue []HandlersUserAPIKeyOut
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeAPIKeysAPIService.ListUserAPIKeys")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/me/api-keys"
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["ApiKeyAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["X-API-KEY"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ctx != nil {
|
||||||
|
// API Key Authentication
|
||||||
|
if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
|
||||||
|
if apiKey, ok := auth["BearerAuth"]; ok {
|
||||||
|
var key string
|
||||||
|
if apiKey.Prefix != "" {
|
||||||
|
key = apiKey.Prefix + " " + apiKey.Key
|
||||||
|
} else {
|
||||||
|
key = apiKey.Key
|
||||||
|
}
|
||||||
|
localVarHeaderParams["Authorization"] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
1476
sdk/go/api_orgs.go
Normal file
1476
sdk/go/api_orgs.go
Normal file
File diff suppressed because it is too large
Load Diff
1065
sdk/go/api_servers.go
Normal file
1065
sdk/go/api_servers.go
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user