mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-10-12 02:11:26 +00:00
[skip ci] Go SDK
This commit is contained in:
+17
-1
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: sudo apt-get install bash curl
|
||||
command: sudo apt-get install bash curl jq
|
||||
|
||||
- run:
|
||||
name: download Go
|
||||
@@ -193,6 +193,22 @@ jobs:
|
||||
npm publish --access public --tag beta
|
||||
fi
|
||||
|
||||
- run:
|
||||
name: Verify OpenAPI spec and Go SDK are up to date
|
||||
command: |
|
||||
bash scripts/generate-api.sh
|
||||
git diff --exit-code api-docs/ go-sdk/client.gen.go go-sdk/version.go || {
|
||||
echo "ERROR: OpenAPI spec or Go SDK is stale. Run scripts/generate-api.sh and commit."
|
||||
exit 1
|
||||
}
|
||||
|
||||
- run:
|
||||
name: Tag Go SDK release
|
||||
command: |
|
||||
VERSION=$(jq -r .version package.json)
|
||||
git tag "go-sdk/v${VERSION}" 2>/dev/null || true
|
||||
git push origin "go-sdk/v${VERSION}" 2>/dev/null || true
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-all:
|
||||
|
||||
+12104
File diff suppressed because it is too large
Load Diff
+10471
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,7 +1,7 @@
|
||||
## Version 0.22
|
||||
- Adding feature to create API key to directly query the Cosmos API
|
||||
- Added SDK to call the API from any project
|
||||
- Refactor the entire permission system for future work on roles and fine-grained permissions
|
||||
- Added GO/TS SDK to call the API from any project (AI generated)
|
||||
- Refactor the entire permission system for future work on roles and fine-grained permissions (partially AI assisted)
|
||||
- You can now create multiple admin users
|
||||
- Fixed issue with post-install on compose import
|
||||
- Support for mounting network disk
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface CreateAPITokenRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
readOnly?: boolean;
|
||||
permissions?: number[];
|
||||
ipWhitelist?: string[];
|
||||
restrictToConstellation?: boolean;
|
||||
}
|
||||
@@ -13,6 +14,13 @@ export interface CreateAPITokenResponse {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateAPITokenRequest {
|
||||
description?: string;
|
||||
permissions?: number[];
|
||||
ipWhitelist?: string[];
|
||||
restrictToConstellation?: boolean;
|
||||
}
|
||||
|
||||
export interface APITokenConfig {
|
||||
Name: string;
|
||||
Description?: string;
|
||||
@@ -44,6 +52,16 @@ export default function createApiTokensAPI(apiFetch: ApiFetch) {
|
||||
}))
|
||||
}
|
||||
|
||||
function update(name: string, config: UpdateAPITokenRequest): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/api-tokens/${encodeURIComponent(name)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
}))
|
||||
}
|
||||
|
||||
function remove(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/api-tokens', {
|
||||
method: 'DELETE',
|
||||
@@ -57,6 +75,7 @@ export default function createApiTokensAPI(apiFetch: ApiFetch) {
|
||||
return {
|
||||
list,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,6 +102,44 @@ export default function createConfigAPI(apiFetch: ApiFetch) {
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Individual Route CRUD ---
|
||||
function listRoutes(): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/routes', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function getRoute(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/routes/${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function createNewRoute(route: Route): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/routes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(route),
|
||||
}))
|
||||
}
|
||||
|
||||
function updateRouteByName(name: string, route: Route): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/routes/${encodeURIComponent(name)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(route),
|
||||
}))
|
||||
}
|
||||
|
||||
function deleteRouteByName(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/routes/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function updateDNS(dnsConfig: {
|
||||
dnsPort?: string;
|
||||
dnsFallback?: string;
|
||||
@@ -132,5 +170,10 @@ export default function createConfigAPI(apiFetch: ApiFetch) {
|
||||
getBackup,
|
||||
getDashboard,
|
||||
updateDNS,
|
||||
listRoutes,
|
||||
getRoute,
|
||||
createNewRoute,
|
||||
updateRouteByName,
|
||||
deleteRouteByName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,6 +182,37 @@ export default function createConstellationAPI(apiFetch: ApiFetch) {
|
||||
}))
|
||||
}
|
||||
|
||||
// --- DNS Entry CRUD ---
|
||||
function listDNSEntries(): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/constellation/dns', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function createDNSEntry(entry: { Type: string; Key: string; Value: string }): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/constellation/dns', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(entry),
|
||||
}))
|
||||
}
|
||||
|
||||
function updateDNSEntry(key: string, entry: { Type: string; Key: string; Value: string }): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/constellation/dns/${encodeURIComponent(key)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(entry),
|
||||
}))
|
||||
}
|
||||
|
||||
function deleteDNSEntry(key: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/constellation/dns/${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
addDevice,
|
||||
@@ -198,5 +229,9 @@ export default function createConstellationAPI(apiFetch: ApiFetch) {
|
||||
tunnels,
|
||||
editDevice,
|
||||
getNextIP,
|
||||
listDNSEntries,
|
||||
createDNSEntry,
|
||||
updateDNSEntry,
|
||||
deleteDNSEntry,
|
||||
};
|
||||
}
|
||||
|
||||
+44
-1
@@ -79,6 +79,44 @@ export default function createCronAPI(apiFetch: ApiFetch, createWs: (path: strin
|
||||
}))
|
||||
}
|
||||
|
||||
// --- CRON Config CRUD (individual cron entries in Config.CRON) ---
|
||||
function listCronConfigs(): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/cron', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function getCronConfig(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/cron/${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function createCronConfig(config: { Name: string; Enabled: boolean; Crontab: string; Command: string; Container?: string }): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/cron', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
}))
|
||||
}
|
||||
|
||||
function updateCronConfig(name: string, config: { Name: string; Enabled: boolean; Crontab: string; Command: string; Container?: string }): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/cron/${encodeURIComponent(name)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
}))
|
||||
}
|
||||
|
||||
function deleteCronConfig(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/cron/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function runningJobs(): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/jobs/running', {
|
||||
method: 'GET',
|
||||
@@ -95,6 +133,11 @@ export default function createCronAPI(apiFetch: ApiFetch, createWs: (path: strin
|
||||
stop,
|
||||
get,
|
||||
deleteJob,
|
||||
runningJobs
|
||||
runningJobs,
|
||||
listCronConfigs,
|
||||
getCronConfig,
|
||||
createCronConfig,
|
||||
updateCronConfig,
|
||||
deleteCronConfig,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import createCronAPI from './cron';
|
||||
import createRcloneAPI from './rclone';
|
||||
import createBackupsAPI from './backup';
|
||||
import createApiTokensAPI from './apiTokens';
|
||||
import createOpenIDAPI from './openid';
|
||||
|
||||
import * as authDemo from './authentication.demo';
|
||||
import * as usersDemo from './users.demo';
|
||||
@@ -53,6 +54,7 @@ export function createClient({ baseUrl, token }) {
|
||||
rclone: createRcloneAPI(apiFetch),
|
||||
backups: createBackupsAPI(apiFetch),
|
||||
apiTokens: createApiTokensAPI(apiFetch),
|
||||
openid: createOpenIDAPI(apiFetch),
|
||||
|
||||
getStatus: () => {
|
||||
return wrap(apiFetch('/cosmos/api/status', {
|
||||
@@ -387,6 +389,7 @@ let cron = createCronAPI(defaultFetch, defaultWs);
|
||||
let rclone = createRcloneAPI(defaultFetch);
|
||||
let backups = createBackupsAPI(defaultFetch);
|
||||
let apiTokens = createApiTokensAPI(defaultFetch);
|
||||
let openid = createOpenIDAPI(defaultFetch);
|
||||
|
||||
if(isDemo) {
|
||||
auth = authDemo;
|
||||
@@ -431,4 +434,5 @@ export {
|
||||
forceAutoUpdate,
|
||||
backups,
|
||||
apiTokens,
|
||||
openid,
|
||||
};
|
||||
|
||||
@@ -37,10 +37,53 @@ export default function createMetricsAPI(apiFetch: ApiFetch) {
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Alert CRUD ---
|
||||
function listAlerts(): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/alerts', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function getAlert(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/alerts/${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function createAlert(alert: any): Promise<ApiResponse> {
|
||||
return wrap(apiFetch('/cosmos/api/alerts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(alert),
|
||||
}))
|
||||
}
|
||||
|
||||
function updateAlert(name: string, alert: any): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/alerts/${encodeURIComponent(name)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(alert),
|
||||
}))
|
||||
}
|
||||
|
||||
function deleteAlert(name: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/alerts/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
get,
|
||||
reset,
|
||||
list,
|
||||
events
|
||||
events,
|
||||
listAlerts,
|
||||
getAlert,
|
||||
createAlert,
|
||||
updateAlert,
|
||||
deleteAlert,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import wrap, { type ApiResponse, type ApiFetch } from './wrap';
|
||||
|
||||
export interface OpenIDClient {
|
||||
id: string;
|
||||
secret: string;
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
export default function createOpenIDAPI(apiFetch: ApiFetch) {
|
||||
function list(): Promise<ApiResponse<OpenIDClient[]>> {
|
||||
return wrap(apiFetch('/cosmos/api/openid', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function get(id: string): Promise<ApiResponse<OpenIDClient>> {
|
||||
return wrap(apiFetch(`/cosmos/api/openid/${encodeURIComponent(id)}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
function create(client: OpenIDClient): Promise<ApiResponse<OpenIDClient>> {
|
||||
return wrap(apiFetch('/cosmos/api/openid', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(client),
|
||||
}))
|
||||
}
|
||||
|
||||
function update(id: string, client: OpenIDClient): Promise<ApiResponse<OpenIDClient>> {
|
||||
return wrap(apiFetch(`/cosmos/api/openid/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(client),
|
||||
}))
|
||||
}
|
||||
|
||||
function remove(id: string): Promise<ApiResponse> {
|
||||
return wrap(apiFetch(`/cosmos/api/openid/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
get,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
+23879
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
// API Token Integration Test Script using the Go SDK
|
||||
// Usage: go run ./cmd/test-token <base_url> <admin_token> <readonly_token>
|
||||
// Example: cd go-sdk && go run ./cmd/test-token https://localhost cosmos_abc123 cosmos_xyz789
|
||||
//
|
||||
// Accepts self-signed certs.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
passed int
|
||||
failed int
|
||||
)
|
||||
|
||||
// apiResponse is the standard Cosmos response shape
|
||||
type apiResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
func test(name string, fn func() error) {
|
||||
if err := fn(); err != nil {
|
||||
failed++
|
||||
fmt.Printf("[FAIL] %s — %s\n", name, err)
|
||||
} else {
|
||||
passed++
|
||||
fmt.Printf("[PASS] %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
func expectStatus(resp *http.Response, expected int, label string) error {
|
||||
if resp.StatusCode != expected {
|
||||
return fmt.Errorf("%s: expected %d, got %d", label, expected, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBody(resp *http.Response) (*apiResponse, error) {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r apiResponse
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(body))
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func expectOK(resp *http.Response) error {
|
||||
r, err := parseBody(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Status != "OK" {
|
||||
return fmt.Errorf("expected status OK, got %s (message: %s)", r.Status, r.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expectForbiddenOrError(resp *http.Response) error {
|
||||
if resp.StatusCode == 403 {
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
r, err := parseBody(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Status == "OK" {
|
||||
return fmt.Errorf("expected error/forbidden, but got OK")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newClient(baseUrl, token string) *cosmossdk.Client {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
addToken := func(ctx context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := cosmossdk.NewClient(baseUrl+"/cosmos", cosmossdk.WithHTTPClient(httpClient), cosmossdk.WithRequestEditorFn(addToken))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func rawRequest(baseUrl, path, method, token string, body interface{}) (*http.Response, error) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
b, _ := json.Marshal(body)
|
||||
bodyReader = strings.NewReader(string(b))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, baseUrl+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return httpClient.Do(req)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: go run test-token.go <base_url> <admin_token> <readonly_token>\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
baseUrl := os.Args[1]
|
||||
adminToken := os.Args[2]
|
||||
readonlyToken := os.Args[3]
|
||||
|
||||
admin := newClient(baseUrl, adminToken)
|
||||
readonly := newClient(baseUrl, readonlyToken)
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Printf("Testing API tokens via Go SDK against %s\n", baseUrl)
|
||||
fmt.Printf("Admin token: %s...\n", adminToken[:12])
|
||||
fmt.Printf("Readonly token: %s...\n", readonlyToken[:12])
|
||||
|
||||
// --- Group 1: SDK Basic Calls ---
|
||||
fmt.Println("\n— Group 1: SDK Basic Calls —")
|
||||
|
||||
test("admin.GetApiStatus()", func() error {
|
||||
resp, err := admin.GetApiStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.GetApiStatus()", func() error {
|
||||
resp, err := readonly.GetApiStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
// --- Group 2: SDK Read Endpoints ---
|
||||
fmt.Println("\n— Group 2: SDK Read Endpoints —")
|
||||
|
||||
test("admin.GetApiConfig()", func() error {
|
||||
resp, err := admin.GetApiConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.GetApiConfig()", func() error {
|
||||
resp, err := readonly.GetApiConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("admin.GetApiServapps()", func() error {
|
||||
resp, err := admin.GetApiServapps(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.GetApiServapps()", func() error {
|
||||
resp, err := readonly.GetApiServapps(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("admin.GetApiUsers()", func() error {
|
||||
resp, err := admin.GetApiUsers(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.GetApiUsers()", func() error {
|
||||
resp, err := readonly.GetApiUsers(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("admin.GetApiApiTokens()", func() error {
|
||||
resp, err := admin.GetApiApiTokens(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.GetApiApiTokens()", func() error {
|
||||
resp, err := readonly.GetApiApiTokens(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
// --- Group 3: SDK Write Endpoints (admin vs read-only) ---
|
||||
fmt.Println("\n— Group 3: SDK Write Endpoints (admin vs read-only) —")
|
||||
|
||||
probeCreated := false
|
||||
|
||||
test("admin.PostApiApiTokens() - create probe", func() error {
|
||||
resp, err := admin.PostApiApiTokens(ctx, cosmossdk.PostApiApiTokensJSONRequestBody{
|
||||
Name: "__test_probe__",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectOK(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
probeCreated = true
|
||||
return nil
|
||||
})
|
||||
|
||||
test("admin.DeleteApiApiTokens() - cleanup probe", func() error {
|
||||
resp, err := admin.DeleteApiApiTokens(ctx, cosmossdk.DeleteApiApiTokensJSONRequestBody{
|
||||
Name: "__test_probe__",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectOK(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
probeCreated = false
|
||||
return nil
|
||||
})
|
||||
|
||||
if probeCreated {
|
||||
// Safety cleanup
|
||||
admin.DeleteApiApiTokens(ctx, cosmossdk.DeleteApiApiTokensJSONRequestBody{
|
||||
Name: "__test_probe__",
|
||||
})
|
||||
}
|
||||
|
||||
test("readonly.PostApiApiTokens() - denied", func() error {
|
||||
resp, err := readonly.PostApiApiTokens(ctx, cosmossdk.PostApiApiTokensJSONRequestBody{
|
||||
Name: "__test_probe__",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectForbiddenOrError(resp)
|
||||
})
|
||||
|
||||
test("readonly.DeleteApiApiTokens() - denied", func() error {
|
||||
resp, err := readonly.DeleteApiApiTokens(ctx, cosmossdk.DeleteApiApiTokensJSONRequestBody{
|
||||
Name: "__test_probe__",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectForbiddenOrError(resp)
|
||||
})
|
||||
|
||||
test("admin.PutApiConfig() - idempotent write-back", func() error {
|
||||
getResp, err := admin.GetApiConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := io.ReadAll(getResp.Body)
|
||||
getResp.Body.Close()
|
||||
|
||||
var parsed apiResponse
|
||||
json.Unmarshal(body, &parsed)
|
||||
|
||||
resp, err := admin.PutApiConfigWithBody(ctx, "application/json", strings.NewReader(string(parsed.Data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectOK(resp)
|
||||
})
|
||||
|
||||
test("readonly.PutApiConfig() - denied", func() error {
|
||||
resp, err := readonly.PutApiConfigWithBody(ctx, "application/json", strings.NewReader("{}"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectForbiddenOrError(resp)
|
||||
})
|
||||
|
||||
// --- Group 4: No-Token Rejection ---
|
||||
fmt.Println("\n— Group 4: No-Token Rejection —")
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
label string
|
||||
}{
|
||||
{"/cosmos/api/config", "config"},
|
||||
{"/cosmos/api/servapps", "servapps"},
|
||||
{"/cosmos/api/users", "users"},
|
||||
} {
|
||||
label := tc.label
|
||||
path := tc.path
|
||||
test(fmt.Sprintf("GET %s - no token rejected", label), func() error {
|
||||
resp, err := rawRequest(baseUrl, path, "GET", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return expectStatus(resp, 401, "no token")
|
||||
})
|
||||
}
|
||||
|
||||
// --- Group 5: Invalid Token ---
|
||||
fmt.Println("\n— Group 5: Invalid Token —")
|
||||
|
||||
test("status - invalid token rejected", func() error {
|
||||
resp, err := rawRequest(baseUrl, "/cosmos/api/status", "GET", "cosmos_invalidtoken", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return expectStatus(resp, 401, "invalid token")
|
||||
})
|
||||
|
||||
// --- Results ---
|
||||
fmt.Printf("\nResults: %d/%d passed\n", passed, passed+failed)
|
||||
if failed > 0 {
|
||||
fmt.Printf("%d test(s) FAILED\n", failed)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module github.com/azukaar/cosmos-server/go-sdk
|
||||
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/getkin/kin-openapi v0.134.0
|
||||
github.com/oapi-codegen/runtime v1.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
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/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU=
|
||||
github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
|
||||
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,7 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
|
||||
package: cosmossdk
|
||||
output: client.gen.go
|
||||
generate:
|
||||
client: true
|
||||
models: true
|
||||
embedded-spec: true
|
||||
@@ -0,0 +1,4 @@
|
||||
package cosmossdk
|
||||
|
||||
// Version is the SDK version, synced with Cosmos Server at build time.
|
||||
const Version = "0.22.0-unstable04"
|
||||
+2
-1
@@ -97,7 +97,8 @@
|
||||
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run --cap-add NET_ADMIN -d -p 7200:443 -p 80:80 -p 53:53 -p 443:443 -p 4242:4242/udp -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
|
||||
"dockerdev": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
|
||||
"demo-build": "vite build --base=/cosmos-ui/ --mode demo",
|
||||
"demo": "vite --mode demo"
|
||||
"demo": "vite --mode demo",
|
||||
"postinstall": "bash scripts/install-hooks.sh"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION=$(jq -r .version package.json)
|
||||
|
||||
echo "Generating API spec and Go SDK for v${VERSION}..."
|
||||
|
||||
# 1. Stamp version into swagger annotation
|
||||
sed -i "s/@version .*/@version $VERSION/" src/index.go
|
||||
|
||||
# 2. Generate Swagger 2.0 spec (using go run — no install needed)
|
||||
go run github.com/swaggo/swag/cmd/swag@latest init -d src -g index.go -o api-docs --outputTypes json,yaml --parseInternal --parseDependency
|
||||
|
||||
# 3. Convert to OpenAPI 3.0
|
||||
npx swagger2openapi api-docs/swagger.json -o api-docs/openapi.json
|
||||
|
||||
# 4. Patch OpenAPI spec: replace Go stdlib types with simple schemas
|
||||
# (os.FileMode, time.Duration, nat.PortSet produce uncompilable generated code)
|
||||
jq '
|
||||
.components.schemas["os.FileMode"] = {"type": "integer", "format": "uint32"} |
|
||||
.components.schemas["time.Duration"] = {"type": "integer", "format": "int64"} |
|
||||
.components.schemas["nat.PortSet"] = {"type": "object", "additionalProperties": true}
|
||||
' api-docs/openapi.json > api-docs/openapi.patched.json
|
||||
mv api-docs/openapi.patched.json api-docs/openapi.json
|
||||
|
||||
# 5. Generate Go SDK client (using go run — no install needed)
|
||||
cd go-sdk
|
||||
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest --config oapi-codegen.yaml ../api-docs/openapi.json
|
||||
|
||||
# 6. Stamp Go SDK version
|
||||
sed -i "s/const Version = \".*\"/const Version = \"$VERSION\"/" version.go
|
||||
|
||||
# 7. Resolve dependencies and verify it compiles
|
||||
go mod tidy
|
||||
go build ./...
|
||||
|
||||
echo "API spec and Go SDK regenerated for v${VERSION}"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")/.."
|
||||
cp scripts/pre-commit .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
echo "Git hooks installed."
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Regenerate API spec + Go SDK if any Go source files are staged
|
||||
|
||||
echo "regenerating OpenAPI spec and Go SDK..."
|
||||
bash scripts/generate-api.sh
|
||||
git add api-docs/ go-sdk/client.gen.go go-sdk/version.go
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@ import createCronAPI from '../../client/src/api/cron';
|
||||
import createRcloneAPI from '../../client/src/api/rclone';
|
||||
import createBackupsAPI from '../../client/src/api/backup';
|
||||
import createApiTokensAPI from '../../client/src/api/apiTokens';
|
||||
import createOpenIDAPI from '../../client/src/api/openid';
|
||||
import wrap from '../../client/src/api/wrap';
|
||||
|
||||
export function createClient({ baseUrl, token }: { baseUrl: string; token: string }) {
|
||||
@@ -29,6 +30,7 @@ export function createClient({ baseUrl, token }: { baseUrl: string; token: strin
|
||||
rclone: createRcloneAPI(apiFetch),
|
||||
backups: createBackupsAPI(apiFetch),
|
||||
apiTokens: createApiTokensAPI(apiFetch),
|
||||
openid: createOpenIDAPI(apiFetch),
|
||||
|
||||
getStatus: () => {
|
||||
return wrap(apiFetch('/cosmos/api/status', {
|
||||
@@ -114,7 +116,8 @@ export type { User } from '../../client/src/api/users';
|
||||
export type { Route, Operation } from '../../client/src/api/config';
|
||||
export type { Container, ContainerState, DockerNetwork, DockerVolume } from '../../client/src/api/docker';
|
||||
export type { MarketApp, MarketResult } from '../../client/src/api/market';
|
||||
export type { CreateAPITokenRequest, CreateAPITokenResponse, APITokenConfig } from '../../client/src/api/apiTokens';
|
||||
export type { CreateAPITokenRequest, CreateAPITokenResponse, UpdateAPITokenRequest, APITokenConfig } from '../../client/src/api/apiTokens';
|
||||
export type { OpenIDClient } from '../../client/src/api/openid';
|
||||
export type { ConstellationDevice } from '../../client/src/api/constellation';
|
||||
export type { CronJob } from '../../client/src/api/cron';
|
||||
export type { DiskInfo, MountRequest, UnmountRequest, MergeRequest, SnapRAIDConfig, RaidCreateRequest } from '../../client/src/api/storage';
|
||||
|
||||
@@ -10,6 +10,19 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// AddBackupRoute godoc
|
||||
// @Summary Create a new backup configuration
|
||||
// @Description Creates a backup config and initializes the repository if needed. Requires premium licence and non-container mode.
|
||||
// @Tags Backups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.SingleBackupConfig true "Backup configuration"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups [post]
|
||||
func AddBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -119,6 +132,17 @@ func AddBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// EditBackupRoute godoc
|
||||
// @Summary Edit an existing backup configuration
|
||||
// @Tags Backups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.SingleBackupConfig true "Updated backup configuration"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/edit [post]
|
||||
func EditBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -158,6 +182,17 @@ func EditBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveBackupRoute godoc
|
||||
// @Summary Remove a backup configuration and its snapshots
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name} [delete]
|
||||
func RemoveBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -237,6 +272,17 @@ func RemoveBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListSnapshotsRoute godoc
|
||||
// @Summary List snapshots for a specific backup
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/snapshots [get]
|
||||
func ListSnapshotsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -276,6 +322,19 @@ func ListSnapshotsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListFoldersRoute godoc
|
||||
// @Summary List folders in a backup snapshot
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Param snapshot path string true "Snapshot ID"
|
||||
// @Param path query string false "Directory path within the snapshot"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/{snapshot}/folders [get]
|
||||
func ListFoldersRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -318,6 +377,20 @@ func ListFoldersRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreBackupRoute godoc
|
||||
// @Summary Restore files from a backup snapshot
|
||||
// @Tags Backups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Param body body object true "Restore request with snapshotId, target, and optional include paths"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/restore [post]
|
||||
func RestoreBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -393,6 +466,17 @@ func RestoreBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListSnapshotsRouteFromRepo godoc
|
||||
// @Summary List all snapshots in a backup repository (all backup tags)
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name (used to resolve repository)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups-repository/{name}/snapshots [get]
|
||||
func ListSnapshotsRouteFromRepo(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -432,6 +516,14 @@ func ListSnapshotsRouteFromRepo(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListRepos godoc
|
||||
// @Summary List all backup repositories with their lock status
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups-repository [get]
|
||||
func ListRepos(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -466,6 +558,17 @@ func ListRepos(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// RepoStatsRoute godoc
|
||||
// @Summary Get repository statistics (size, file count, etc.)
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name (used to resolve repository)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups-repository/{name}/stats [get]
|
||||
func RepoStatsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -504,6 +607,18 @@ func RepoStatsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ForgetSnapshotRoute godoc
|
||||
// @Summary Forget (delete) a specific snapshot from a backup repository
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Param snapshot path string true "Snapshot ID to forget"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/{snapshot}/forget [delete]
|
||||
func ForgetSnapshotRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -537,6 +652,19 @@ func ForgetSnapshotRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// StatsRepositorySubfolderRoute godoc
|
||||
// @Summary Get restore size stats for a subfolder within a snapshot
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Param snapshot path string true "Snapshot ID"
|
||||
// @Param path query string false "Subfolder path within the snapshot"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/{snapshot}/subfolder-restore-size [get]
|
||||
func StatsRepositorySubfolderRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -578,6 +706,17 @@ func StatsRepositorySubfolderRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockRepositoryRoute godoc
|
||||
// @Summary Unlock a backup repository (remove stale locks)
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups/{name}/unlock [post]
|
||||
func UnlockRepositoryRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
+150
-7
@@ -12,20 +12,29 @@ import (
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/constellation"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var validTokenName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
|
||||
type CreateAPITokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
IPWhitelist []string `json:"ipWhitelist,omitempty"`
|
||||
RestrictToConstellation bool `json:"restrictToConstellation"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
Permissions []utils.Permission `json:"permissions,omitempty"`
|
||||
IPWhitelist []string `json:"ipWhitelist,omitempty"`
|
||||
RestrictToConstellation bool `json:"restrictToConstellation"`
|
||||
}
|
||||
|
||||
type DeleteAPITokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateAPITokenRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Permissions []utils.Permission `json:"permissions,omitempty"`
|
||||
IPWhitelist []string `json:"ipWhitelist,omitempty"`
|
||||
RestrictToConstellation *bool `json:"restrictToConstellation,omitempty"`
|
||||
}
|
||||
|
||||
func APITokenRoute(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -41,6 +50,16 @@ func APITokenRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// listAPITokens godoc
|
||||
// @Summary List all API tokens
|
||||
// @Description Returns all configured API tokens (without the actual token hashes)
|
||||
// @Tags api-tokens
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/api-tokens [get]
|
||||
func listAPITokens(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN_READ) != nil {
|
||||
return
|
||||
@@ -67,6 +86,21 @@ func listAPITokens(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// createAPIToken godoc
|
||||
// @Summary Create a new API token
|
||||
// @Description Generates a new API token with the specified permissions and returns the raw token (shown only once)
|
||||
// @Tags api-tokens
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body CreateAPITokenRequest true "Token creation details"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/api-tokens [post]
|
||||
func createAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
@@ -104,7 +138,9 @@ func createAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Build permissions array
|
||||
var permissions []utils.Permission
|
||||
if request.ReadOnly {
|
||||
if len(request.Permissions) > 0 {
|
||||
permissions = request.Permissions
|
||||
} else if request.ReadOnly {
|
||||
permissions = []utils.Permission{
|
||||
utils.PERM_ADMIN_READ,
|
||||
utils.PERM_USERS_READ,
|
||||
@@ -179,6 +215,20 @@ func createAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// deleteAPIToken godoc
|
||||
// @Summary Delete an API token
|
||||
// @Description Removes an API token by name
|
||||
// @Tags api-tokens
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body DeleteAPITokenRequest true "Token name to delete"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/api-tokens [delete]
|
||||
func deleteAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
@@ -231,3 +281,96 @@ func deleteAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func APITokenIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "PUT" {
|
||||
updateAPIToken(w, req)
|
||||
} else {
|
||||
utils.Error("APITokenIdRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// updateAPIToken godoc
|
||||
// @Summary Update an API token
|
||||
// @Description Updates an existing API token's description, permissions, IP whitelist, or constellation restriction
|
||||
// @Tags api-tokens
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Token name"
|
||||
// @Param request body UpdateAPITokenRequest true "Fields to update"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/api-tokens/{name} [put]
|
||||
func updateAPIToken(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := mux.Vars(req)["name"]
|
||||
if name == "" {
|
||||
utils.HTTPError(w, "Token name is required", http.StatusBadRequest, "AT030")
|
||||
return
|
||||
}
|
||||
|
||||
var request UpdateAPITokenRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("UpdateAPIToken: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "AT031")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.APITokens == nil {
|
||||
utils.HTTPError(w, "Token not found", http.StatusNotFound, "AT032")
|
||||
return
|
||||
}
|
||||
|
||||
token, exists := config.APITokens[name]
|
||||
if !exists {
|
||||
utils.HTTPError(w, "Token not found", http.StatusNotFound, "AT032")
|
||||
return
|
||||
}
|
||||
|
||||
// Update only the fields that were provided — token hash stays the same
|
||||
if request.Description != nil {
|
||||
token.Description = *request.Description
|
||||
}
|
||||
if request.Permissions != nil {
|
||||
token.Permissions = request.Permissions
|
||||
}
|
||||
if request.IPWhitelist != nil {
|
||||
token.IPWhitelist = request.IPWhitelist
|
||||
}
|
||||
if request.RestrictToConstellation != nil {
|
||||
token.RestrictToConstellation = *request.RestrictToConstellation
|
||||
}
|
||||
|
||||
config.APITokens[name] = token
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TouchDatabase()
|
||||
go constellation.SendNewDBSyncMessage()
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.api.token.updated",
|
||||
"API Token updated",
|
||||
"important",
|
||||
"token@"+name,
|
||||
map[string]interface{}{
|
||||
"tokenName": name,
|
||||
},
|
||||
)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,6 +25,17 @@ type DashboardRoute struct {
|
||||
ContainerIcon string `json:"ContainerIcon,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardApiGet godoc
|
||||
// @Summary Get dashboard data
|
||||
// @Description Returns dashboard route information with container status for SERVAPP routes
|
||||
// @Tags config
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse{data=[]DashboardRoute}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/dashboard [get]
|
||||
func DashboardApiGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -16,6 +16,20 @@ type DNSConfigRequest struct {
|
||||
CustomDNSEntries []utils.ConstellationDNSEntry `json:"customDNSEntries,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigApiDNS godoc
|
||||
// @Summary Update DNS configuration
|
||||
// @Description Patches DNS-related settings including port, fallback, blocklists, and custom entries
|
||||
// @Tags config
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body DNSConfigRequest true "DNS configuration fields to update"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/config/dns [patch]
|
||||
func ConfigApiDNS(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
|
||||
+22
-1
@@ -8,6 +8,16 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ConfigApiGet godoc
|
||||
// @Summary Get server configuration
|
||||
// @Description Returns the full server configuration (sensitive fields masked for non-credential users)
|
||||
// @Tags config
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.Config}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/config [get]
|
||||
func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
@@ -33,7 +43,6 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
|
||||
config.HTTPConfig.DNSChallengeConfig = map[string]string{}
|
||||
config.Licence = "***"
|
||||
config.ServerToken = "***"
|
||||
config.APITokens = map[string]utils.APITokenConfig{}
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
@@ -61,6 +70,18 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// BackupFileApiGet godoc
|
||||
// @Summary Get backup file
|
||||
// @Description Returns the backup cosmos-compose JSON file
|
||||
// @Tags config
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/get-backup [get]
|
||||
func BackupFileApiGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CREDENTIALS_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
package configapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func OpenIDRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
listOpenIDClients(w, req)
|
||||
} else if req.Method == "POST" {
|
||||
createOpenIDClient(w, req)
|
||||
} else {
|
||||
utils.Error("OpenIDRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
func OpenIDIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
getOpenIDClient(w, req)
|
||||
} else if req.Method == "PUT" {
|
||||
updateOpenIDClient(w, req)
|
||||
} else if req.Method == "DELETE" {
|
||||
deleteOpenIDClient(w, req)
|
||||
} else {
|
||||
utils.Error("OpenIDIdRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// listOpenIDClients godoc
|
||||
// @Summary List all OpenID clients
|
||||
// @Description Returns all configured OpenID Connect client configurations
|
||||
// @Tags openid
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse{data=[]utils.OpenIDClient}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/openid [get]
|
||||
func listOpenIDClients(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.OpenIDClients,
|
||||
})
|
||||
}
|
||||
|
||||
// getOpenIDClient godoc
|
||||
// @Summary Get an OpenID client by ID
|
||||
// @Description Returns a single OpenID Connect client configuration
|
||||
// @Tags openid
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "OpenID client ID"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.OpenIDClient}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/openid/{id} [get]
|
||||
func getOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(req)["id"]
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
for _, client := range config.OpenIDClients {
|
||||
if client.ID == id {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": client,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.HTTPError(w, "OpenID client not found", http.StatusNotFound, "OID001")
|
||||
}
|
||||
|
||||
// createOpenIDClient godoc
|
||||
// @Summary Create an OpenID client
|
||||
// @Description Creates a new OpenID Connect client configuration
|
||||
// @Tags openid
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body utils.OpenIDClient true "OpenID client configuration"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.OpenIDClient}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Router /api/openid [post]
|
||||
func createOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newClient utils.OpenIDClient
|
||||
err := json.NewDecoder(req.Body).Decode(&newClient)
|
||||
if err != nil {
|
||||
utils.Error("CreateOpenIDClient: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "OID002")
|
||||
return
|
||||
}
|
||||
|
||||
if newClient.ID == "" {
|
||||
utils.HTTPError(w, "OpenID client ID is required", http.StatusBadRequest, "OID003")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
clients := config.OpenIDClients
|
||||
|
||||
for _, client := range clients {
|
||||
if client.ID == newClient.ID {
|
||||
utils.HTTPError(w, "OpenID client with this ID already exists", http.StatusConflict, "OID004")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config.OpenIDClients = append(config.OpenIDClients, newClient)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"OpenID client created: "+newClient.ID,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": newClient,
|
||||
})
|
||||
}
|
||||
|
||||
// updateOpenIDClient godoc
|
||||
// @Summary Update an OpenID client
|
||||
// @Description Replaces an existing OpenID Connect client configuration
|
||||
// @Tags openid
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "OpenID client ID"
|
||||
// @Param request body utils.OpenIDClient true "Updated OpenID client configuration"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.OpenIDClient}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/openid/{id} [put]
|
||||
func updateOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(req)["id"]
|
||||
|
||||
var updatedClient utils.OpenIDClient
|
||||
err := json.NewDecoder(req.Body).Decode(&updatedClient)
|
||||
if err != nil {
|
||||
utils.Error("UpdateOpenIDClient: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "OID005")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
clients := config.OpenIDClients
|
||||
clientIndex := -1
|
||||
|
||||
for i, client := range clients {
|
||||
if client.ID == id {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if clientIndex == -1 {
|
||||
utils.HTTPError(w, "OpenID client not found", http.StatusNotFound, "OID006")
|
||||
return
|
||||
}
|
||||
|
||||
clients[clientIndex] = updatedClient
|
||||
config.OpenIDClients = clients
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"OpenID client updated: "+id,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": updatedClient,
|
||||
})
|
||||
}
|
||||
|
||||
// deleteOpenIDClient godoc
|
||||
// @Summary Delete an OpenID client
|
||||
// @Description Removes an OpenID Connect client configuration by ID
|
||||
// @Tags openid
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "OpenID client ID"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/openid/{id} [delete]
|
||||
func deleteOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(req)["id"]
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
clients := config.OpenIDClients
|
||||
clientIndex := -1
|
||||
|
||||
for i, client := range clients {
|
||||
if client.ID == id {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if clientIndex == -1 {
|
||||
utils.HTTPError(w, "OpenID client not found", http.StatusNotFound, "OID007")
|
||||
return
|
||||
}
|
||||
|
||||
clients = append(clients[:clientIndex], clients[clientIndex+1:]...)
|
||||
config.OpenIDClients = clients
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"OpenID client deleted: "+id,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
+15
-1
@@ -10,10 +10,24 @@ import (
|
||||
|
||||
type UpdateRouteRequest struct {
|
||||
RouteName string `json:"routeName"`
|
||||
Operation string `json:"operation"`
|
||||
Operation string `json:"operation" validate:"required"`
|
||||
NewRoute *utils.ProxyRouteConfig `json:"newRoute,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigApiPatch godoc
|
||||
// @Summary Patch route configuration
|
||||
// @Description Performs route operations (add, replace, delete, move_up, move_down) on proxy routes
|
||||
// @Tags config
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body UpdateRouteRequest true "Route update operation"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/config [patch]
|
||||
func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
|
||||
@@ -6,6 +6,17 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ConfigApiRestart godoc
|
||||
// @Summary Restart the server
|
||||
// @Description Triggers a graceful server restart
|
||||
// @Tags config
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/restart [get]
|
||||
func ConfigApiRestart(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package configapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func RoutesRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
listRoutes(w, req)
|
||||
} else if req.Method == "POST" {
|
||||
createRoute(w, req)
|
||||
} else {
|
||||
utils.Error("RoutesRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
func RoutesIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
getRoute(w, req)
|
||||
} else if req.Method == "PUT" {
|
||||
updateRoute(w, req)
|
||||
} else if req.Method == "DELETE" {
|
||||
deleteRoute(w, req)
|
||||
} else {
|
||||
utils.Error("RoutesIdRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// listRoutes godoc
|
||||
// @Summary List all proxy routes
|
||||
// @Description Returns all configured proxy routes
|
||||
// @Tags routes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse{data=[]utils.ProxyRouteConfig}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/routes [get]
|
||||
func listRoutes(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.HTTPConfig.ProxyConfig.Routes,
|
||||
})
|
||||
}
|
||||
|
||||
// getRoute godoc
|
||||
// @Summary Get a proxy route by name
|
||||
// @Description Returns a single proxy route configuration by its name
|
||||
// @Tags routes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Route name"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.ProxyRouteConfig}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/routes/{name} [get]
|
||||
func getRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := mux.Vars(req)["name"]
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
for _, route := range config.HTTPConfig.ProxyConfig.Routes {
|
||||
if route.Name == name {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": route,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.HTTPError(w, "Route not found", http.StatusNotFound, "RT001")
|
||||
}
|
||||
|
||||
// createRoute godoc
|
||||
// @Summary Create a new proxy route
|
||||
// @Description Creates a new proxy route configuration
|
||||
// @Tags routes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body utils.ProxyRouteConfig true "Route configuration"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.ProxyRouteConfig}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Router /api/routes [post]
|
||||
func createRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newRoute utils.ProxyRouteConfig
|
||||
err := json.NewDecoder(req.Body).Decode(&newRoute)
|
||||
if err != nil {
|
||||
utils.Error("CreateRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "RT002")
|
||||
return
|
||||
}
|
||||
|
||||
if newRoute.Name == "" {
|
||||
utils.HTTPError(w, "Route name is required", http.StatusBadRequest, "RT003")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||
|
||||
for _, route := range routes {
|
||||
if route.Name == newRoute.Name {
|
||||
utils.HTTPError(w, "Route with this name already exists", http.StatusConflict, "RT004")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config.HTTPConfig.ProxyConfig.Routes = append([]utils.ProxyRouteConfig{newRoute}, routes...)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route created: "+newRoute.Name,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
go func() {
|
||||
utils.RestartHTTPServer()
|
||||
}()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": newRoute,
|
||||
})
|
||||
}
|
||||
|
||||
// updateRoute godoc
|
||||
// @Summary Update a proxy route
|
||||
// @Description Replaces an existing proxy route configuration by name
|
||||
// @Tags routes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Route name"
|
||||
// @Param request body utils.ProxyRouteConfig true "Updated route configuration"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.ProxyRouteConfig}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/routes/{name} [put]
|
||||
func updateRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := mux.Vars(req)["name"]
|
||||
|
||||
var updatedRoute utils.ProxyRouteConfig
|
||||
err := json.NewDecoder(req.Body).Decode(&updatedRoute)
|
||||
if err != nil {
|
||||
utils.Error("UpdateRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "RT005")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||
routeIndex := -1
|
||||
|
||||
for i, route := range routes {
|
||||
if route.Name == name {
|
||||
routeIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if routeIndex == -1 {
|
||||
utils.HTTPError(w, "Route not found", http.StatusNotFound, "RT006")
|
||||
return
|
||||
}
|
||||
|
||||
routes[routeIndex] = updatedRoute
|
||||
config.HTTPConfig.ProxyConfig.Routes = routes
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route updated: "+name,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
go func() {
|
||||
utils.RestartHTTPServer()
|
||||
}()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": updatedRoute,
|
||||
})
|
||||
}
|
||||
|
||||
// deleteRoute godoc
|
||||
// @Summary Delete a proxy route
|
||||
// @Description Removes a proxy route configuration by name
|
||||
// @Tags routes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Route name"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/routes/{name} [delete]
|
||||
func deleteRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := mux.Vars(req)["name"]
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||
routeIndex := -1
|
||||
|
||||
for i, route := range routes {
|
||||
if route.Name == name {
|
||||
routeIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if routeIndex == -1 {
|
||||
utils.HTTPError(w, "Route not found", http.StatusNotFound, "RT007")
|
||||
return
|
||||
}
|
||||
|
||||
routes = append(routes[:routeIndex], routes[routeIndex+1:]...)
|
||||
config.HTTPConfig.ProxyConfig.Routes = routes
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route deleted: "+name,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
go func() {
|
||||
utils.RestartHTTPServer()
|
||||
}()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,20 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ConfigApiSet godoc
|
||||
// @Summary Update server configuration
|
||||
// @Description Replaces the entire server configuration (masked credential fields are preserved from existing config)
|
||||
// @Tags config
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body utils.Config true "Full configuration object"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/config [put]
|
||||
func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
|
||||
@@ -9,11 +9,22 @@ import (
|
||||
)
|
||||
|
||||
type DeviceBlockRequestJSON struct {
|
||||
Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
|
||||
DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
|
||||
Block bool `json:"block",omitempty`
|
||||
Nickname string `json:"nickname" validate:"required,min=3,max=32,alphanum"`
|
||||
DeviceName string `json:"deviceName" validate:"required,min=3,max=32,alphanum"`
|
||||
Block bool `json:"block,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceBlock godoc
|
||||
// @Summary Block or unblock a Constellation device
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body DeviceBlockRequestJSON true "Device block/unblock payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/block [post]
|
||||
func DeviceBlock(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.FBL.AgentMode {
|
||||
utils.Error("Constellation: Agents cannot manage devices. Use a manager server", nil)
|
||||
|
||||
@@ -11,27 +11,38 @@ import (
|
||||
)
|
||||
|
||||
type DeviceCreateRequestJSON struct {
|
||||
DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
|
||||
IP string `json:"ip",validate:"required,ipv4"`
|
||||
PublicKey string `json:"publicKey",omitempty`
|
||||
DeviceName string `json:"deviceName" validate:"required,min=3,max=32,alphanum"`
|
||||
IP string `json:"ip" validate:"required,ipv4"`
|
||||
PublicKey string `json:"publicKey,omitempty"`
|
||||
|
||||
// for devices only
|
||||
Nickname string `json:"nickname",validate:"max=32,alphanum",omitempty`
|
||||
Invisible bool `json:"invisible",omitempty`
|
||||
Nickname string `json:"nickname,omitempty" validate:"max=32,alphanum"`
|
||||
Invisible bool `json:"invisible,omitempty"`
|
||||
|
||||
// for lighthouse only
|
||||
IsLighthouse bool `json:"isLighthouse",omitempty`
|
||||
CosmosNode int `json:"cosmosNode",omitempty`
|
||||
IsRelay bool `json:"isRelay",omitempty`
|
||||
IsLoadBalancer bool `json:"isLoadBalancer",omitempty`
|
||||
IsExitNode bool `json:"isExitNode",omitempty`
|
||||
PublicHostname string `json:"PublicHostname",omitempty`
|
||||
Port string `json:"port",omitempty`
|
||||
IsLighthouse bool `json:"isLighthouse,omitempty"`
|
||||
CosmosNode int `json:"cosmosNode,omitempty"`
|
||||
IsRelay bool `json:"isRelay,omitempty"`
|
||||
IsLoadBalancer bool `json:"isLoadBalancer,omitempty"`
|
||||
IsExitNode bool `json:"isExitNode,omitempty"`
|
||||
PublicHostname string `json:"PublicHostname,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
|
||||
// internal
|
||||
APIKey string `json:"-"`
|
||||
}
|
||||
|
||||
// DeviceCreate_API godoc
|
||||
// @Summary Create a new Constellation device and generate its certificates
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body DeviceCreateRequestJSON true "Device creation payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/devices [post]
|
||||
func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "POST") {
|
||||
var request DeviceCreateRequestJSON
|
||||
|
||||
@@ -15,6 +15,17 @@ type DeviceEditRequestJSON struct {
|
||||
IsLoadBalancer bool `json:"isLoadBalancer"`
|
||||
}
|
||||
|
||||
// DeviceEdit_API godoc
|
||||
// @Summary Edit the current Constellation device properties
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body DeviceEditRequestJSON true "Device edit payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/edit-device [post]
|
||||
func DeviceEdit_API(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "POST" {
|
||||
utils.Error("DeviceEdit: Method not allowed "+req.Method, nil)
|
||||
|
||||
@@ -7,6 +7,15 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// DeviceList godoc
|
||||
// @Summary List Constellation devices for the current user (or all if admin)
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/devices [get]
|
||||
func DeviceList(w http.ResponseWriter, req *http.Request) {
|
||||
// Check for GET method
|
||||
if req.Method != "GET" {
|
||||
|
||||
@@ -10,6 +10,18 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// DevicePing godoc
|
||||
// @Summary Ping a Constellation device to check reachability
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Param id path string true "Device name/ID"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/devices/{id}/ping [get]
|
||||
func DevicePing(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
|
||||
|
||||
@@ -24,6 +24,15 @@ type PublicDeviceInfo struct {
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
// DevicePublicList godoc
|
||||
// @Summary List public information about all non-blocked Constellation devices
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/public-devices [get]
|
||||
func DevicePublicList(w http.ResponseWriter, req *http.Request) {
|
||||
// Check for GET method
|
||||
if req.Method != "GET" {
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func DNSEntriesRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
listDNSEntries(w, req)
|
||||
} else if req.Method == "POST" {
|
||||
createDNSEntry(w, req)
|
||||
} else {
|
||||
utils.Error("DNSEntriesRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
func DNSEntriesIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
getDNSEntry(w, req)
|
||||
} else if req.Method == "PUT" {
|
||||
updateDNSEntry(w, req)
|
||||
} else if req.Method == "DELETE" {
|
||||
deleteDNSEntry(w, req)
|
||||
} else {
|
||||
utils.Error("DNSEntriesIdRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// listDNSEntries godoc
|
||||
// @Summary List all custom Constellation DNS entries
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/dns [get]
|
||||
func listDNSEntries(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.ConstellationConfig.CustomDNSEntries,
|
||||
})
|
||||
}
|
||||
|
||||
// getDNSEntry godoc
|
||||
// @Summary Get a single Constellation DNS entry by key
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Param key path string true "DNS entry key"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/dns/{key} [get]
|
||||
func getDNSEntry(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := mux.Vars(req)["key"]
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
for _, entry := range config.ConstellationConfig.CustomDNSEntries {
|
||||
if entry.Key == key {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": entry,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.HTTPError(w, "DNS entry not found", http.StatusNotFound, "DNS001")
|
||||
}
|
||||
|
||||
// createDNSEntry godoc
|
||||
// @Summary Create a new Constellation DNS entry
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.ConstellationDNSEntry true "DNS entry to create"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/dns [post]
|
||||
func createDNSEntry(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newEntry utils.ConstellationDNSEntry
|
||||
err := json.NewDecoder(req.Body).Decode(&newEntry)
|
||||
if err != nil {
|
||||
utils.Error("CreateDNSEntry: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "DNS002")
|
||||
return
|
||||
}
|
||||
|
||||
if newEntry.Key == "" {
|
||||
utils.HTTPError(w, "DNS entry key is required", http.StatusBadRequest, "DNS003")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
entries := config.ConstellationConfig.CustomDNSEntries
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Key == newEntry.Key {
|
||||
utils.HTTPError(w, "DNS entry with this key already exists", http.StatusConflict, "DNS004")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config.ConstellationConfig.CustomDNSEntries = append(config.ConstellationConfig.CustomDNSEntries, newEntry)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"DNS entry created: "+newEntry.Key,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": newEntry,
|
||||
})
|
||||
}
|
||||
|
||||
// updateDNSEntry godoc
|
||||
// @Summary Update an existing Constellation DNS entry
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param key path string true "DNS entry key"
|
||||
// @Param body body utils.ConstellationDNSEntry true "Updated DNS entry"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/dns/{key} [put]
|
||||
func updateDNSEntry(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := mux.Vars(req)["key"]
|
||||
|
||||
var updatedEntry utils.ConstellationDNSEntry
|
||||
err := json.NewDecoder(req.Body).Decode(&updatedEntry)
|
||||
if err != nil {
|
||||
utils.Error("UpdateDNSEntry: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "DNS005")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
entries := config.ConstellationConfig.CustomDNSEntries
|
||||
entryIndex := -1
|
||||
|
||||
for i, entry := range entries {
|
||||
if entry.Key == key {
|
||||
entryIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if entryIndex == -1 {
|
||||
utils.HTTPError(w, "DNS entry not found", http.StatusNotFound, "DNS006")
|
||||
return
|
||||
}
|
||||
|
||||
entries[entryIndex] = updatedEntry
|
||||
config.ConstellationConfig.CustomDNSEntries = entries
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"DNS entry updated: "+key,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": updatedEntry,
|
||||
})
|
||||
}
|
||||
|
||||
// deleteDNSEntry godoc
|
||||
// @Summary Delete a Constellation DNS entry by key
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Param key path string true "DNS entry key"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/dns/{key} [delete]
|
||||
func deleteDNSEntry(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := mux.Vars(req)["key"]
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
entries := config.ConstellationConfig.CustomDNSEntries
|
||||
entryIndex := -1
|
||||
|
||||
for i, entry := range entries {
|
||||
if entry.Key == key {
|
||||
entryIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if entryIndex == -1 {
|
||||
utils.HTTPError(w, "DNS entry not found", http.StatusNotFound, "DNS007")
|
||||
return
|
||||
}
|
||||
|
||||
entries = append(entries[:entryIndex], entries[entryIndex+1:]...)
|
||||
config.ConstellationConfig.CustomDNSEntries = entries
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"DNS entry deleted: "+key,
|
||||
"success",
|
||||
"",
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
@@ -85,6 +85,15 @@ func GetNextAvailableIP(cidr string) string {
|
||||
return getNextAvailableIP(usedIPs, cidr)
|
||||
}
|
||||
|
||||
// API_GetNextIP godoc
|
||||
// @Summary Get the next available IP address in the Constellation CIDR range
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/get-next-ip [get]
|
||||
func API_GetNextIP(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" {
|
||||
utils.Error("API_GetNextIP: Method not allowed "+req.Method, nil)
|
||||
|
||||
@@ -54,6 +54,15 @@ func CheckConstellationToken(req *http.Request) error {
|
||||
return errors.New("Unauthorized: Client not found")
|
||||
}
|
||||
|
||||
// API_GetConfig godoc
|
||||
// @Summary Get the current Nebula configuration
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/config [get]
|
||||
func API_GetConfig(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -80,6 +89,14 @@ func API_GetConfig(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_Restart godoc
|
||||
// @Summary Restart the Nebula VPN service and HTTP server
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/restart [get]
|
||||
func API_Restart(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -101,6 +118,14 @@ func API_Restart(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_Reset godoc
|
||||
// @Summary Reset the Nebula VPN configuration
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/reset [get]
|
||||
func API_Reset(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -121,6 +146,15 @@ func API_Reset(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_GetLogs godoc
|
||||
// @Summary Get Nebula VPN service logs
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/logs [get]
|
||||
func API_GetLogs(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -144,6 +178,14 @@ func API_GetLogs(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_Ping godoc
|
||||
// @Summary Check if the NATS client connection is alive
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/ping [get]
|
||||
func API_Ping(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,18 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// API_NewConstellation godoc
|
||||
// @Summary Create a new Constellation VPN network
|
||||
// @Tags constellation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Constellation creation payload (deviceName, isLighthouse, hostname, ipRange)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/create [post]
|
||||
func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -144,6 +156,17 @@ func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_ConnectToExisting godoc
|
||||
// @Summary Connect this node to an existing Constellation VPN network
|
||||
// @Tags constellation
|
||||
// @Accept application/x-yaml
|
||||
// @Produce json
|
||||
// @Param body body string true "Nebula YAML configuration"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/connect [post]
|
||||
func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -7,6 +7,14 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// TunnelList godoc
|
||||
// @Summary List all active Constellation tunnels
|
||||
// @Tags constellation
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/constellation/tunnels [get]
|
||||
func TunnelList(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
|
||||
|
||||
@@ -8,6 +8,14 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ListJobs godoc
|
||||
// @Summary List all scheduled jobs
|
||||
// @Tags Cron
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs [get]
|
||||
func ListJobs(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -57,6 +65,17 @@ type JobRequestJSON struct {
|
||||
}
|
||||
|
||||
|
||||
// StopJobRoute godoc
|
||||
// @Summary Stop a running job
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body JobRequestJSON true "Job identifier"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs/stop [post]
|
||||
func StopJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -92,6 +111,17 @@ func StopJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteJobRoute godoc
|
||||
// @Summary Delete a job from the CRON configuration
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body JobRequestJSON true "Job identifier"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs/delete [post]
|
||||
func DeleteJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -131,6 +161,17 @@ func DeleteJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// RunJobRoute godoc
|
||||
// @Summary Manually trigger a job to run
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body JobRequestJSON true "Job identifier"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs/run [post]
|
||||
func RunJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -169,6 +210,18 @@ func RunJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetJobRoute godoc
|
||||
// @Summary Get details and logs of a specific job
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body JobRequestJSON true "Job identifier"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs/get [post]
|
||||
func GetJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -214,6 +267,14 @@ func GetJobRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetRunningJobsRoute godoc
|
||||
// @Summary List currently running jobs
|
||||
// @Tags Cron
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/jobs/running [get]
|
||||
func GetRunningJobsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// CronConfigRoute godoc
|
||||
// @Summary List or create CRON job configurations
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.CRONConfig false "CRON config (POST only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Router /api/cron [get]
|
||||
// @Router /api/cron [post]
|
||||
func CronConfigRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
cronConfigList(w, req)
|
||||
} else if req.Method == "POST" {
|
||||
cronConfigCreate(w, req)
|
||||
} else {
|
||||
utils.Error("CronConfigRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// CronConfigIdRoute godoc
|
||||
// @Summary Get, update, or delete a CRON job configuration by name
|
||||
// @Tags Cron
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "CRON job name"
|
||||
// @Param body body utils.CRONConfig false "Updated CRON config (PUT only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/cron/{name} [get]
|
||||
// @Router /api/cron/{name} [put]
|
||||
// @Router /api/cron/{name} [delete]
|
||||
func CronConfigIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
cronConfigGet(w, req)
|
||||
} else if req.Method == "PUT" {
|
||||
cronConfigUpdate(w, req)
|
||||
} else if req.Method == "DELETE" {
|
||||
cronConfigDelete(w, req)
|
||||
} else {
|
||||
utils.Error("CronConfigIdRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
func cronConfigList(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.CRON,
|
||||
})
|
||||
}
|
||||
|
||||
func cronConfigGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
|
||||
cronJob, exists := config.CRON[name]
|
||||
if !exists {
|
||||
utils.HTTPError(w, "CRON job not found", http.StatusNotFound, "CR001")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": cronJob,
|
||||
})
|
||||
}
|
||||
|
||||
func cronConfigCreate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cronJob utils.CRONConfig
|
||||
if err := json.NewDecoder(req.Body).Decode(&cronJob); err != nil {
|
||||
utils.Error("CronConfigCreate: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "CR010")
|
||||
return
|
||||
}
|
||||
|
||||
if cronJob.Name == "" {
|
||||
utils.HTTPError(w, "CRON job name is required", http.StatusBadRequest, "CR011")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.CRON == nil {
|
||||
config.CRON = make(map[string]utils.CRONConfig)
|
||||
}
|
||||
|
||||
if _, exists := config.CRON[cronJob.Name]; exists {
|
||||
utils.HTTPError(w, "CRON job with this name already exists", http.StatusConflict, "CR012")
|
||||
return
|
||||
}
|
||||
|
||||
config.CRON[cronJob.Name] = cronJob
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.cron.config.created",
|
||||
"CRON job config created",
|
||||
"important",
|
||||
"cron@"+cronJob.Name,
|
||||
map[string]interface{}{
|
||||
"name": cronJob.Name,
|
||||
},
|
||||
)
|
||||
|
||||
go (func() {
|
||||
InitJobs()
|
||||
InitScheduler()
|
||||
})()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": cronJob,
|
||||
})
|
||||
}
|
||||
|
||||
func cronConfigUpdate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
var cronJob utils.CRONConfig
|
||||
if err := json.NewDecoder(req.Body).Decode(&cronJob); err != nil {
|
||||
utils.Error("CronConfigUpdate: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "CR020")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.CRON == nil {
|
||||
utils.HTTPError(w, "CRON job not found", http.StatusNotFound, "CR021")
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := config.CRON[name]; !exists {
|
||||
utils.HTTPError(w, "CRON job not found", http.StatusNotFound, "CR021")
|
||||
return
|
||||
}
|
||||
|
||||
// If the name changed, remove the old entry
|
||||
if cronJob.Name != "" && cronJob.Name != name {
|
||||
delete(config.CRON, name)
|
||||
config.CRON[cronJob.Name] = cronJob
|
||||
} else {
|
||||
cronJob.Name = name
|
||||
config.CRON[name] = cronJob
|
||||
}
|
||||
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.cron.config.updated",
|
||||
"CRON job config updated",
|
||||
"important",
|
||||
"cron@"+name,
|
||||
map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
go (func() {
|
||||
InitJobs()
|
||||
InitScheduler()
|
||||
})()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": cronJob,
|
||||
})
|
||||
}
|
||||
|
||||
func cronConfigDelete(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.CRON == nil {
|
||||
utils.HTTPError(w, "CRON job not found", http.StatusNotFound, "CR030")
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := config.CRON[name]; !exists {
|
||||
utils.HTTPError(w, "CRON job not found", http.StatusNotFound, "CR030")
|
||||
return
|
||||
}
|
||||
|
||||
delete(config.CRON, name)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.cron.config.deleted",
|
||||
"CRON job config deleted",
|
||||
"important",
|
||||
"cron@"+name,
|
||||
map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
go (func() {
|
||||
InitJobs()
|
||||
InitScheduler()
|
||||
})()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
+22
@@ -9,6 +9,18 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// CheckDNSRoute godoc
|
||||
// @Summary Check DNS configuration for a URL
|
||||
// @Description Verifies that DNS is correctly configured for the given URL
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param url query string true "URL to check DNS for"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/dns-check [get]
|
||||
func CheckDNSRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
@@ -47,6 +59,16 @@ func CheckDNSRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
|
||||
// GetDNSRoute godoc
|
||||
// @Summary Resolve DNS for a URL
|
||||
// @Description Performs a DNS lookup for the given URL and returns the resolved IPv4 address
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Param url query string true "URL to resolve"
|
||||
// @Success 200 {object} utils.APIResponse{data=string}
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/dns [get]
|
||||
func GetDNSRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if !utils.GetMainConfig().NewInstall && (utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil) {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,17 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AutoUpdateContainerRoute godoc
|
||||
// @Summary Enable or disable auto-update for a Docker container
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param status path string true "Auto-update status (true or false)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/auto-update/{status} [get]
|
||||
func AutoUpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -45,7 +45,7 @@ type ContainerCreateRequestContainerDependsOnCont struct {
|
||||
|
||||
type ContainerCreateRequestContainer struct {
|
||||
Name string `json:"container_name"`
|
||||
Image string `json:"image"`
|
||||
Image string `json:"image" validate:"required"`
|
||||
Environment []string `json:"environment"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Ports []string `json:"ports"`
|
||||
@@ -219,6 +219,18 @@ func Rollback(actions []DockerServiceCreateRollback , OnLog func(string)) {
|
||||
OnLog("[OPERATION FAILED]. CHANGES HAVE BEEN ROLLEDBACK.\n")
|
||||
}
|
||||
|
||||
// CreateServiceRoute godoc
|
||||
// @Summary Create a Docker service (compose-like) with networks, volumes, and containers
|
||||
// @Tags docker
|
||||
// @Accept json
|
||||
// @Produce plain
|
||||
// @Param body body DockerServiceCreateRequest true "Service creation payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {string} string "Streamed creation output"
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/docker-service [post]
|
||||
func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -12,6 +12,16 @@ import (
|
||||
|
||||
var maxLimit = 1000
|
||||
|
||||
// ListContainersRoute godoc
|
||||
// @Summary List all Docker containers
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param limit query int false "Maximum number of containers to return"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps [get]
|
||||
func ListContainersRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -44,6 +54,16 @@ func ListContainersRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExportContainerRoute godoc
|
||||
// @Summary Export a container configuration as a service definition
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/export [get]
|
||||
func ExportContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CREDENTIALS_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,16 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// GetContainerRoute godoc
|
||||
// @Summary Inspect a single Docker container by ID
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/ [get]
|
||||
func GetContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -78,6 +78,20 @@ func FilterLogs(logReader io.Reader, searchQuery string, limit int) []LogOutput
|
||||
return logLines
|
||||
}
|
||||
|
||||
// GetContainerLogsRoute godoc
|
||||
// @Summary Retrieve logs for a Docker container
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param limit query int false "Maximum number of log lines to return"
|
||||
// @Param search query string false "Filter logs by search string"
|
||||
// @Param lastReceivedLogs query string false "Timestamp to fetch logs until"
|
||||
// @Param errorOnly query bool false "Return only stderr logs"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/logs [get]
|
||||
func GetContainerLogsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -8,6 +8,16 @@ import (
|
||||
|
||||
)
|
||||
|
||||
// InspectImageRoute godoc
|
||||
// @Summary Inspect a Docker image by name
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param imageName query string true "Name of the Docker image to inspect"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} types.ImageInspect
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/images [get]
|
||||
func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -13,6 +13,19 @@ import (
|
||||
contstuff "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// ManageContainerRoute godoc
|
||||
// @Summary Perform a management action on a Docker container
|
||||
// @Description Actions: stop, start, restart, kill, remove, pause, unpause, recreate, update
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param action path string true "Management action" Enums(stop, start, restart, kill, remove, pause, unpause, recreate, update)
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/manage/{action} [get]
|
||||
func ManageContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -8,10 +8,22 @@ import (
|
||||
)
|
||||
|
||||
type migrateToHostMode struct {
|
||||
HTTPPort string `json:"http"`
|
||||
HTTPSPort string `json:"https"`
|
||||
HTTPPort string `json:"http" validate:"required"`
|
||||
HTTPSPort string `json:"https" validate:"required"`
|
||||
}
|
||||
|
||||
// MigrateToHostModeRoute godoc
|
||||
// @Summary Migrate Cosmos container to host network mode
|
||||
// @Tags docker
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body migrateToHostMode true "HTTP and HTTPS port configuration"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/migrate-host [post]
|
||||
func MigrateToHostModeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -12,6 +12,15 @@ import (
|
||||
network "github.com/docker/docker/api/types/network"
|
||||
)
|
||||
|
||||
// ListNetworksRoute godoc
|
||||
// @Summary List all Docker networks
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/networks [get]
|
||||
func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -45,6 +54,16 @@ func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
|
||||
// DeleteNetworkRoute godoc
|
||||
// @Summary Delete a Docker network by ID
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param networkID path string true "Network ID"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/network/{networkID} [delete]
|
||||
func DeleteNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -126,6 +145,17 @@ func NetworkRoutes(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// AttachNetwork godoc
|
||||
// @Summary Attach a Docker network to a container
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param networkId path string true "Network ID"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/network/{networkId} [post]
|
||||
func AttachNetwork(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -161,6 +191,18 @@ func AttachNetwork(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// DetachNetwork godoc
|
||||
// @Summary Detach a Docker network from a container
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param networkId path string true "Network ID"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/network/{networkId} [delete]
|
||||
func DetachNetwork(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -202,6 +244,16 @@ func DetachNetwork(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListContainerNetworks godoc
|
||||
// @Summary List all networks and the networks a container is connected to
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/networks [get]
|
||||
func ListContainerNetworks(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -249,13 +301,25 @@ func ListContainerNetworks(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
type createNetworkPayload struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Driver string `json:"driver"`
|
||||
AttachCosmos bool `json:"attachCosmos"`
|
||||
ParentInterface string `json:"parentInterface"`
|
||||
Subnet string `json:"subnet"`
|
||||
}
|
||||
|
||||
// CreateNetworkRoute godoc
|
||||
// @Summary Create a new Docker network
|
||||
// @Tags docker
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createNetworkPayload true "Network creation payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/networks [post]
|
||||
func CreateNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,18 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SecureContainerRoute godoc
|
||||
// @Summary Set or unset network isolation on a Docker container
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param status path string true "Network secured status (true or false)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/secure/{status} [get]
|
||||
func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,16 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// PullImageIfMissing godoc
|
||||
// @Summary Pull a Docker image only if it is not already present locally
|
||||
// @Tags docker
|
||||
// @Produce plain
|
||||
// @Param imageName query string true "Name of the Docker image to pull"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {string} string "Streamed pull output"
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/images/pull-if-missing [get]
|
||||
func PullImageIfMissing(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -74,6 +84,16 @@ func PullImageIfMissing(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// PullImage godoc
|
||||
// @Summary Pull a Docker image from a registry
|
||||
// @Tags docker
|
||||
// @Produce plain
|
||||
// @Param imageName query string true "Name of the Docker image to pull"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {string} string "Streamed pull output"
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/images/pull [get]
|
||||
func PullImage(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -130,6 +150,16 @@ func PullImage(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// CanUpdateImageRoute godoc
|
||||
// @Summary Check whether a container's image has a newer version available
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/check-update [get]
|
||||
func CanUpdateImageRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -31,6 +31,19 @@ type ContainerForm struct {
|
||||
CPUs float64 `json:"cpus"`
|
||||
}
|
||||
|
||||
// UpdateContainerRoute godoc
|
||||
// @Summary Update a Docker container's settings
|
||||
// @Tags docker
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param containerId path string true "Container ID or name"
|
||||
// @Param body body ContainerForm true "Container update payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/servapps/{containerId}/update [post]
|
||||
func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,15 @@ import (
|
||||
volumeTypes"github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
// ListVolumeRoute godoc
|
||||
// @Summary List all Docker volumes
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/volumes [get]
|
||||
func ListVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -42,6 +51,16 @@ func ListVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteVolumeRoute godoc
|
||||
// @Summary Delete a Docker volume by name
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param volumeName path string true "Name of the volume to delete"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/volume/{volumeName} [delete]
|
||||
func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -79,10 +98,22 @@ func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
type VolumeCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Driver string `json:"driver"`
|
||||
}
|
||||
|
||||
// CreateVolumeRoute godoc
|
||||
// @Summary Create a new Docker volume
|
||||
// @Tags docker
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body VolumeCreateRequest true "Volume creation payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/volumes [post]
|
||||
func CreateVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -654,8 +654,17 @@ func InitServer() *mux.Router {
|
||||
srapiAdmin.HandleFunc("/api/image/{name}", GetImage)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/get-backup", configapi.BackupFileApiGet)
|
||||
srapiAdmin.HandleFunc("/api/api-tokens/{name}", configapi.APITokenIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/api-tokens", configapi.APITokenRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/routes/{name}", configapi.RoutesIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/routes", configapi.RoutesRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/openid/{id}", configapi.OpenIDIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/openid", configapi.OpenIDRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/constellation/dns/{key}", constellation.DNSEntriesIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/constellation/dns", constellation.DNSEntriesRoute)
|
||||
srapiAdmin.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
|
||||
srapiAdmin.HandleFunc("/api/constellation/public-devices", constellation.DevicePublicList)
|
||||
srapiAdmin.HandleFunc("/api/constellation/devices/{id}/ping", constellation.DevicePing)
|
||||
@@ -673,6 +682,9 @@ func InitServer() *mux.Router {
|
||||
|
||||
srapiAdmin.HandleFunc("/api/events", metrics.API_ListEvents)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/alerts/{name}", metrics.AlertsIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/alerts", metrics.AlertsRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/metrics", metrics.API_GetMetrics)
|
||||
srapiAdmin.HandleFunc("/api/reset-metrics", metrics.API_ResetMetrics)
|
||||
srapiAdmin.HandleFunc("/api/list-metrics", metrics.ListMetrics)
|
||||
@@ -680,6 +692,9 @@ func InitServer() *mux.Router {
|
||||
srapiAdmin.HandleFunc("/api/notifications/read", utils.MarkAsRead)
|
||||
srapiAdmin.HandleFunc("/api/notifications", utils.NotifGet)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/cron/{name}", cron.CronConfigIdRoute)
|
||||
srapiAdmin.HandleFunc("/api/cron", cron.CronConfigRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/listen=jobs", cron.ListJobs)
|
||||
srapiAdmin.HandleFunc("/api/jobs", cron.ListJobs)
|
||||
srapiAdmin.HandleFunc("/api/jobs/stop", cron.StopJobRoute)
|
||||
|
||||
@@ -76,6 +76,19 @@ type result struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
// GetFavicon godoc
|
||||
// @Summary Get favicon for a URL
|
||||
// @Description Fetches and caches the favicon for the given URL, returning it as an image
|
||||
// @Tags system
|
||||
// @Produce octet-stream
|
||||
// @Security BearerAuth
|
||||
// @Param q query string true "URL-encoded site URL to fetch favicon for"
|
||||
// @Param servapp query string false "Service app mode flag"
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/favicon [get]
|
||||
func GetFavicon(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
@@ -268,6 +281,19 @@ func resolveIconURL(iconURL string, baseURL *url.URL) string {
|
||||
}
|
||||
|
||||
|
||||
// PingURL godoc
|
||||
// @Summary Ping a URL
|
||||
// @Description Checks if a URL is reachable and returns a non-5xx response
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param q query string true "URL-encoded site URL to ping"
|
||||
// @Param servapp query string false "Service app mode flag"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/ping [get]
|
||||
func PingURL(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
@@ -329,6 +355,14 @@ func PingURL(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// SendLogo godoc
|
||||
// @Summary Get Cosmos logo
|
||||
// @Description Returns the Cosmos server logo as a PNG image
|
||||
// @Tags system
|
||||
// @Produce png
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /logo [get]
|
||||
func SendLogo(w http.ResponseWriter, req *http.Request) {
|
||||
pwd,_ := os.Getwd()
|
||||
imgsrc := "Logo2.png"
|
||||
|
||||
@@ -27,6 +27,22 @@ var validExtensions = map[string]bool{
|
||||
".avif": true,
|
||||
}
|
||||
|
||||
// UploadImage godoc
|
||||
// @Summary Upload an image
|
||||
// @Description Uploads an image file to the server (supports jpg, jpeg, png, gif, bmp, svg, webp, tiff, avif)
|
||||
// @Tags system
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Base name for the uploaded file"
|
||||
// @Param image formData file true "Image file to upload"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/upload/{name} [post]
|
||||
func UploadImage(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
@@ -104,6 +120,19 @@ func UploadImage(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetImage godoc
|
||||
// @Summary Get an uploaded image
|
||||
// @Description Returns a previously uploaded image by name
|
||||
// @Tags system
|
||||
// @Produce octet-stream
|
||||
// @Security BearerAuth
|
||||
// @Param name path string true "Image file name (including extension)"
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/image/{name} [get]
|
||||
func GetImage(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
|
||||
@@ -200,6 +200,13 @@ func main() {
|
||||
cosmos()
|
||||
}
|
||||
|
||||
// @title Cosmos Server API
|
||||
// @version 0.22.0-unstable04
|
||||
// @description REST API for Cosmos Cloud server management
|
||||
// @BasePath /cosmos
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
func cosmos() {
|
||||
utils.PushShieldMetrics = metrics.PushShieldMetrics
|
||||
utils.GetContainerIPByName = docker.GetContainerIPByName
|
||||
|
||||
@@ -12,6 +12,16 @@ type marketGetResult struct {
|
||||
All map[string]interface{} `json:"all"`
|
||||
}
|
||||
|
||||
// MarketGet godoc
|
||||
// @Summary Get the app marketplace listings
|
||||
// @Description Returns all marketplace sources and their applications, plus the showcase from cosmos-cloud.
|
||||
// @Tags Market
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/markets [get]
|
||||
func MarketGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,15 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// API_GetMetrics godoc
|
||||
// @Summary Get aggregated metrics data
|
||||
// @Tags Metrics
|
||||
// @Produce json
|
||||
// @Param metrics query string false "Comma-separated list of metric keys to retrieve"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/metrics [get]
|
||||
func API_GetMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -41,6 +50,15 @@ func API_GetMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API_ResetMetrics godoc
|
||||
// @Summary Reset all metrics and events data
|
||||
// @Tags Metrics
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/reset-metrics [get]
|
||||
func API_ResetMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -92,6 +110,15 @@ type MetricList struct {
|
||||
Label string
|
||||
}
|
||||
|
||||
// ListMetrics godoc
|
||||
// @Summary List all available metric keys and their labels
|
||||
// @Tags Metrics
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/list-metrics [get]
|
||||
func ListMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AlertsRoute godoc
|
||||
// @Summary List or create monitoring alerts
|
||||
// @Tags Metrics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.Alert false "Alert config (POST only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Router /api/alerts [get]
|
||||
// @Router /api/alerts [post]
|
||||
func AlertsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
alertsList(w, req)
|
||||
} else if req.Method == "POST" {
|
||||
alertsCreate(w, req)
|
||||
} else {
|
||||
utils.Error("AlertsRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// AlertsIdRoute godoc
|
||||
// @Summary Get, update, or delete a monitoring alert by name
|
||||
// @Tags Metrics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "Alert name"
|
||||
// @Param body body utils.Alert false "Updated alert config (PUT only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/alerts/{name} [get]
|
||||
// @Router /api/alerts/{name} [put]
|
||||
// @Router /api/alerts/{name} [delete]
|
||||
func AlertsIdRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "GET" {
|
||||
alertsGet(w, req)
|
||||
} else if req.Method == "PUT" {
|
||||
alertsUpdate(w, req)
|
||||
} else if req.Method == "DELETE" {
|
||||
alertsDelete(w, req)
|
||||
} else {
|
||||
utils.Error("AlertsIdRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
func alertsList(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.MonitoringAlerts,
|
||||
})
|
||||
}
|
||||
|
||||
func alertsGet(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION_READ) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
|
||||
alert, exists := config.MonitoringAlerts[name]
|
||||
if !exists {
|
||||
utils.HTTPError(w, "Alert not found", http.StatusNotFound, "AL001")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": alert,
|
||||
})
|
||||
}
|
||||
|
||||
func alertsCreate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var alert utils.Alert
|
||||
if err := json.NewDecoder(req.Body).Decode(&alert); err != nil {
|
||||
utils.Error("AlertsCreate: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "AL010")
|
||||
return
|
||||
}
|
||||
|
||||
if alert.Name == "" {
|
||||
utils.HTTPError(w, "Alert name is required", http.StatusBadRequest, "AL011")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.MonitoringAlerts == nil {
|
||||
config.MonitoringAlerts = make(map[string]utils.Alert)
|
||||
}
|
||||
|
||||
if _, exists := config.MonitoringAlerts[alert.Name]; exists {
|
||||
utils.HTTPError(w, "Alert with this name already exists", http.StatusConflict, "AL012")
|
||||
return
|
||||
}
|
||||
|
||||
config.MonitoringAlerts[alert.Name] = alert
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.alerts.created",
|
||||
"Alert created",
|
||||
"important",
|
||||
"alert@"+alert.Name,
|
||||
map[string]interface{}{
|
||||
"name": alert.Name,
|
||||
},
|
||||
)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": alert,
|
||||
})
|
||||
}
|
||||
|
||||
func alertsUpdate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
var alert utils.Alert
|
||||
if err := json.NewDecoder(req.Body).Decode(&alert); err != nil {
|
||||
utils.Error("AlertsUpdate: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request", http.StatusBadRequest, "AL020")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.MonitoringAlerts == nil {
|
||||
utils.HTTPError(w, "Alert not found", http.StatusNotFound, "AL021")
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := config.MonitoringAlerts[name]; !exists {
|
||||
utils.HTTPError(w, "Alert not found", http.StatusNotFound, "AL021")
|
||||
return
|
||||
}
|
||||
|
||||
// If the name changed, remove the old entry
|
||||
if alert.Name != "" && alert.Name != name {
|
||||
delete(config.MonitoringAlerts, name)
|
||||
config.MonitoringAlerts[alert.Name] = alert
|
||||
} else {
|
||||
alert.Name = name
|
||||
config.MonitoringAlerts[name] = alert
|
||||
}
|
||||
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.alerts.updated",
|
||||
"Alert updated",
|
||||
"important",
|
||||
"alert@"+name,
|
||||
map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": alert,
|
||||
})
|
||||
}
|
||||
|
||||
func alertsDelete(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CONFIGURATION) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
if config.MonitoringAlerts == nil {
|
||||
utils.HTTPError(w, "Alert not found", http.StatusNotFound, "AL030")
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := config.MonitoringAlerts[name]; !exists {
|
||||
utils.HTTPError(w, "Alert not found", http.StatusNotFound, "AL030")
|
||||
return
|
||||
}
|
||||
|
||||
delete(config.MonitoringAlerts, name)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.alerts.deleted",
|
||||
"Alert deleted",
|
||||
"important",
|
||||
"alert@"+name,
|
||||
map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
@@ -23,6 +23,21 @@ type Event struct {
|
||||
Object string `json:"object" bson:"object"`
|
||||
}
|
||||
|
||||
// API_ListEvents godoc
|
||||
// @Summary List events with filtering and pagination
|
||||
// @Tags Metrics
|
||||
// @Produce json
|
||||
// @Param from query string false "Start date in RFC3339 format (2006-01-02T15:04:05Z)"
|
||||
// @Param to query string false "End date in RFC3339 format (2006-01-02T15:04:05Z)"
|
||||
// @Param logLevel query string false "Minimum log level (debug, info, success, warning, important, error)" default(info)
|
||||
// @Param search query string false "Full-text search term"
|
||||
// @Param query query string false "BSON query as JSON string"
|
||||
// @Param page query string false "Pagination cursor (ObjectID hex string)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/events [get]
|
||||
func API_ListEvents(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -43,6 +43,18 @@ type AdminJSON struct {
|
||||
Password string `validate:"required,min=9,max=128,containsany=~!@#$%^&*()_+=-{[}]:;"'<>.?/,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"`
|
||||
}
|
||||
|
||||
// NewInstallRoute godoc
|
||||
// @Summary Process new installation steps
|
||||
// @Description Handles multi-step server installation including database setup, HTTPS configuration, and admin user creation
|
||||
// @Tags system
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body NewInstallJSON true "Installation step data"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/newInstall [post]
|
||||
func NewInstallRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if !utils.GetMainConfig().NewInstall {
|
||||
utils.Error("Status: not a new New install", nil)
|
||||
|
||||
+85
-7
@@ -11,6 +11,15 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ListDisksRoute godoc
|
||||
// @Summary List all disks
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/disks [get]
|
||||
func ListDisksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -35,6 +44,15 @@ func ListDisksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ListMountsRoute godoc
|
||||
// @Summary List all mounted filesystems
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/mounts [get]
|
||||
func ListMountsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -61,14 +79,25 @@ func ListMountsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Assuming the structure for the mount/unmount request
|
||||
type MountRequest struct {
|
||||
Path string `json:"path"`
|
||||
MountPoint string `json:"mountPoint"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
MountPoint string `json:"mountPoint" validate:"required"`
|
||||
Permanent bool `json:"permanent"`
|
||||
NetDisk bool `json:"netDisk"`
|
||||
Chown string `json:"chown"`
|
||||
}
|
||||
|
||||
// MountRoute handles mounting filesystem requests
|
||||
// MountRoute godoc
|
||||
// @Summary Mount a filesystem
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body MountRequest true "Mount request"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/mount [post]
|
||||
func MountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -100,7 +129,18 @@ func MountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmountRoute handles unmounting filesystem requests
|
||||
// UnmountRoute godoc
|
||||
// @Summary Unmount a filesystem
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body MountRequest true "Unmount request"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/unmount [post]
|
||||
func UnmountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -134,14 +174,25 @@ func UnmountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Assuming the structure for the mount/unmount request
|
||||
type MergeRequest struct {
|
||||
Branches []string `json:"branches"`
|
||||
MountPoint string `json:"mountPoint"`
|
||||
Branches []string `json:"branches" validate:"required"`
|
||||
MountPoint string `json:"mountPoint" validate:"required"`
|
||||
Permanent bool `json:"permanent"`
|
||||
Chown string `json:"chown"`
|
||||
Opts string `json:"opts"`
|
||||
}
|
||||
|
||||
// MergeRoute handles merging filesystem requests
|
||||
// MergeRoute godoc
|
||||
// @Summary Merge multiple filesystems using MergerFS
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body MergeRequest true "Merge request"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/merge [post]
|
||||
func MergeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -237,6 +288,20 @@ func snapRAIDDeleteRoute(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// SnapRAIDEditRoute godoc
|
||||
// @Summary Update or delete a SnapRAID configuration by name
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "SnapRAID config name"
|
||||
// @Param body body utils.SnapRAIDConfig false "Updated SnapRAID config (POST only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/snapraid/{name} [post]
|
||||
// @Router /api/snapraid/{name} [delete]
|
||||
func SnapRAIDEditRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -283,6 +348,19 @@ func listSNAPRaidRoute(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// SNAPRaidCRUDRoute godoc
|
||||
// @Summary List or create SnapRAID configurations
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body utils.SnapRAIDConfig false "SnapRAID config (POST only)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/snapraid [get]
|
||||
// @Router /api/snapraid [post]
|
||||
func SNAPRaidCRUDRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if req.Method == "GET" {
|
||||
|
||||
@@ -8,7 +8,18 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// UnmountRoute handles unmounting filesystem requests
|
||||
// SnapRAIDRunRoute godoc
|
||||
// @Summary Run a SnapRAID action (sync, scrub, fix, enable, disable)
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Param name path string true "SnapRAID config name"
|
||||
// @Param action path string true "Action to run" Enums(sync, scrub, fix, enable, disable)
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/snapraid/{name}/{action} [get]
|
||||
func SnapRAIDRunRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
type FormatDiskJSON struct {
|
||||
Disk string `json:"disk"`
|
||||
Format string `json:"format"`
|
||||
Password string `json:"password"`
|
||||
Disk string `json:"disk" validate:"required"`
|
||||
Format string `json:"format" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
func isDiskOrPartition(path string) (string, error) {
|
||||
@@ -42,6 +42,20 @@ func isDiskOrPartition(path string) (string, error) {
|
||||
|
||||
var diskOpMutex sync.Mutex
|
||||
|
||||
// FormatDiskRoute godoc
|
||||
// @Summary Format a disk or partition
|
||||
// @Description Formats the specified disk with the given filesystem format. Requires password confirmation. Streams progress output.
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce plain
|
||||
// @Param body body FormatDiskJSON true "Format disk request"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {string} string "Streamed text output ending with [OPERATION SUCCEEDED]"
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/disks/format [post]
|
||||
func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -8,6 +8,14 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
)
|
||||
|
||||
// ListSmartDef godoc
|
||||
// @Summary Get SMART attribute definitions for ATA and NVMe drives
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/smart-def [get]
|
||||
func ListSmartDef(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -29,6 +29,18 @@ type DirectoryListing struct {
|
||||
FullPath string `json:"fullPath"`
|
||||
}
|
||||
|
||||
// ListDirectoryRoute godoc
|
||||
// @Summary List the contents of a directory
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Param storage query string false "Storage name (default: local)"
|
||||
// @Param path query string false "Directory path (default: /)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/list-dir [get]
|
||||
func ListDirectoryRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -160,6 +172,19 @@ func ListDirectory(path string) ([]DirectoryListing, error) {
|
||||
return listings, nil
|
||||
}
|
||||
|
||||
// CreateFolderRoute godoc
|
||||
// @Summary Create a new folder
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Param storage query string false "Storage name"
|
||||
// @Param path query string false "Parent directory path"
|
||||
// @Param folder query string true "Folder name to create"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/new-dir [post]
|
||||
func CreateFolderRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
|
||||
// RaidConfig represents the configuration for creating a new RAID array
|
||||
type RaidConfig struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Level int `json:"level"` // 0, 1, 5, 6, 10
|
||||
Devices []string `json:"devices"` // List of device paths
|
||||
Devices []string `json:"devices" validate:"required"` // List of device paths
|
||||
Spares []string `json:"spares"` // List of spare device paths
|
||||
Metadata string `json:"metadata"` // Metadata version (default: 1.2)
|
||||
Filesystem string `json:"filesystem"` // Filesystem to use (default: ext4)
|
||||
@@ -300,7 +300,7 @@ func RaidDeleteRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
type RaidDeviceRequest struct {
|
||||
Device string `json:"device"`
|
||||
Device string `json:"device" validate:"required"`
|
||||
}
|
||||
|
||||
func RaidAddDeviceRoute(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -338,8 +338,8 @@ func RaidAddDeviceRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
type RaidReplaceDeviceRequest struct {
|
||||
OldDevice string `json:"oldDevice"`
|
||||
NewDevice string `json:"newDevice"`
|
||||
OldDevice string `json:"oldDevice" validate:"required"`
|
||||
NewDevice string `json:"newDevice" validate:"required"`
|
||||
}
|
||||
|
||||
func RaidReplaceDeviceRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
@@ -445,6 +445,14 @@ func remountAll() {
|
||||
utils.Exec("smbcontrol", "all", "reload-config")
|
||||
}
|
||||
|
||||
// API_Rclone_remountAll godoc
|
||||
// @Summary Restart all RClone remote storage mounts
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/rclone-restart [get]
|
||||
func API_Rclone_remountAll(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
|
||||
+92
-10
@@ -36,7 +36,14 @@ func ensureRCloneConfig() {
|
||||
rcloneConfigInitialized = true
|
||||
}
|
||||
|
||||
// API_RClone_ConfigDump handles /cosmos/rclone/config/dump
|
||||
// API_RClone_ConfigDump godoc
|
||||
// @Summary Dump the full RClone configuration
|
||||
// @Tags RClone
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/dump [get]
|
||||
func API_RClone_ConfigDump(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_CREDENTIALS_READ) != nil {
|
||||
return
|
||||
@@ -48,8 +55,14 @@ func API_RClone_ConfigDump(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// API_RClone_ListRemotes handles /cosmos/rclone/config/listremotes
|
||||
// Returns only remote name + type, no credentials
|
||||
// API_RClone_ListRemotes godoc
|
||||
// @Summary List RClone remotes (name and type only, no credentials)
|
||||
// @Tags RClone
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/listremotes [get]
|
||||
func API_RClone_ListRemotes(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -70,7 +83,18 @@ func API_RClone_ListRemotes(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(remotes)
|
||||
}
|
||||
|
||||
// API_RClone_ConfigCreate handles /cosmos/rclone/config/create
|
||||
// API_RClone_ConfigCreate godoc
|
||||
// @Summary Create a new RClone remote
|
||||
// @Tags RClone
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Remote config with name, type, and parameters"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/create [post]
|
||||
func API_RClone_ConfigCreate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -110,7 +134,18 @@ func API_RClone_ConfigCreate(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
// API_RClone_ConfigUpdate handles /cosmos/rclone/config/update
|
||||
// API_RClone_ConfigUpdate godoc
|
||||
// @Summary Update an existing RClone remote
|
||||
// @Tags RClone
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Remote update with name and parameters"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/update [put]
|
||||
func API_RClone_ConfigUpdate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -149,7 +184,17 @@ func API_RClone_ConfigUpdate(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
// API_RClone_ConfigDelete handles /cosmos/rclone/config/delete
|
||||
// API_RClone_ConfigDelete godoc
|
||||
// @Summary Delete an RClone remote
|
||||
// @Tags RClone
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Object with name of remote to delete"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/delete [delete]
|
||||
func API_RClone_ConfigDelete(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -175,7 +220,14 @@ func API_RClone_ConfigDelete(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
// API_RClone_ConfigSave handles /cosmos/rclone/config/save
|
||||
// API_RClone_ConfigSave godoc
|
||||
// @Summary Save the RClone configuration
|
||||
// @Tags RClone
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/config/save [post]
|
||||
func API_RClone_ConfigSave(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
@@ -185,7 +237,19 @@ func API_RClone_ConfigSave(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
// API_RClone_OperationsAbout handles /cosmos/rclone/operations/about (ping storage)
|
||||
// API_RClone_OperationsAbout godoc
|
||||
// @Summary Get storage usage info for a remote (total, used, free)
|
||||
// @Tags RClone
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Object with fs field specifying the remote"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Failure 501 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/operations/about [post]
|
||||
func API_RClone_OperationsAbout(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -241,7 +305,17 @@ func API_RClone_OperationsAbout(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// API_RClone_VfsStats handles /cosmos/rclone/vfs/stats
|
||||
// API_RClone_VfsStats godoc
|
||||
// @Summary Get VFS stats for a mounted remote
|
||||
// @Tags RClone
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body object true "Object with fs field specifying the remote"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/vfs/stats [post]
|
||||
func API_RClone_VfsStats(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
@@ -284,7 +358,15 @@ func API_RClone_VfsStats(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
// API_RClone_CoreStats handles /cosmos/rclone/core/stats
|
||||
// API_RClone_CoreStats godoc
|
||||
// @Summary Get global RClone transfer stats
|
||||
// @Tags RClone
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /rclone/core/stats [get]
|
||||
func API_RClone_CoreStats(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -22,6 +22,15 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/cron"
|
||||
)
|
||||
|
||||
// StatusRoute godoc
|
||||
// @Summary Get server status
|
||||
// @Description Returns the current server status including configuration, resource info, and system state
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/status [get]
|
||||
func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||
config := utils.GetMainConfig()
|
||||
|
||||
@@ -108,6 +117,14 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// CanSendEmail godoc
|
||||
// @Summary Check if email sending is enabled
|
||||
// @Description Returns whether the server has email sending capabilities configured
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/can-send-email [get]
|
||||
func CanSendEmail(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "GET") {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
@@ -151,6 +168,17 @@ func getRealSizeOf2(v interface{}) (int) {
|
||||
return b.Len()
|
||||
}
|
||||
|
||||
// MemStatusRoute godoc
|
||||
// @Summary Get internal memory status
|
||||
// @Description Returns memory usage details for internal caches and buffers
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/_memory [get]
|
||||
func MemStatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if (utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil) {
|
||||
return
|
||||
@@ -179,6 +207,17 @@ func MemStatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogsRoute godoc
|
||||
// @Summary Download server logs
|
||||
// @Description Returns the server log file as a plain text attachment
|
||||
// @Tags system
|
||||
// @Produce plain
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /_logs [get]
|
||||
func LogsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN_READ) != nil {
|
||||
return
|
||||
@@ -216,6 +255,17 @@ func LogsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ForceUpdateRoute godoc
|
||||
// @Summary Force check for server updates
|
||||
// @Description Triggers a manual check for available server updates
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/force-server-update [post]
|
||||
func ForceUpdateRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
@@ -231,6 +281,17 @@ func ForceUpdateRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// restartHostMachineRoute godoc
|
||||
// @Summary Restart the host machine
|
||||
// @Description Triggers a restart of the host machine (not available when running inside a container)
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/restart-server [get]
|
||||
func restartHostMachineRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_ADMIN) != nil {
|
||||
return
|
||||
|
||||
+13
-1
@@ -9,9 +9,21 @@ import (
|
||||
)
|
||||
|
||||
type User2FACheckRequest struct {
|
||||
Token string
|
||||
Token string `validate:"required"`
|
||||
}
|
||||
|
||||
// Check2FA godoc
|
||||
// @Summary Verify 2FA token
|
||||
// @Description Validates a TOTP token for the current user to complete multi-factor authentication
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body User2FACheckRequest true "TOTP token"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/mfa [post]
|
||||
func Check2FA(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN_WEAK) != nil {
|
||||
return
|
||||
|
||||
@@ -10,6 +10,16 @@ import (
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// New2FA godoc
|
||||
// @Summary Generate new 2FA key
|
||||
// @Description Generates a new TOTP key for the current user and returns the provisioning URL
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/mfa [get]
|
||||
func New2FA(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN_WEAK) != nil {
|
||||
return
|
||||
|
||||
+14
-1
@@ -8,9 +8,22 @@ import (
|
||||
)
|
||||
|
||||
type User2FAResetRequest struct {
|
||||
Nickname string
|
||||
Nickname string `validate:"required"`
|
||||
}
|
||||
|
||||
// Delete2FA godoc
|
||||
// @Summary Reset 2FA for a user
|
||||
// @Description Removes the 2FA key for a specified user (admin operation)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body User2FAResetRequest true "Nickname of the user to reset 2FA for"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/mfa [delete]
|
||||
func Delete2FA(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_USERS) != nil {
|
||||
return
|
||||
|
||||
@@ -15,6 +15,21 @@ type CreateRequestJSON struct {
|
||||
Role utils.Role `json:"role"`
|
||||
}
|
||||
|
||||
// UserCreate godoc
|
||||
// @Summary Create a new user
|
||||
// @Description Creates a new user account with a registration key for invite-based onboarding
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body CreateRequestJSON true "User creation details"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 409 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/users [post]
|
||||
func UserCreate(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_USERS) != nil {
|
||||
return
|
||||
|
||||
@@ -8,6 +8,19 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// UserDelete godoc
|
||||
// @Summary Delete a user
|
||||
// @Description Deletes a user account by nickname
|
||||
// @Tags users
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param nickname path string true "User nickname"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/users/{nickname} [delete]
|
||||
func UserDelete(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
nickname := utils.Sanitize(vars["nickname"])
|
||||
|
||||
@@ -12,6 +12,21 @@ type EditRequestJSON struct {
|
||||
Role *utils.Role `json:"role"`
|
||||
}
|
||||
|
||||
// UserEdit godoc
|
||||
// @Summary Edit a user
|
||||
// @Description Updates user details such as email and role
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param nickname path string true "User nickname"
|
||||
// @Param request body EditRequestJSON true "Fields to update"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/users/{nickname} [patch]
|
||||
func UserEdit(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
nickname := utils.Sanitize(vars["nickname"])
|
||||
|
||||
@@ -7,6 +7,19 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// UserGet godoc
|
||||
// @Summary Get a user by nickname
|
||||
// @Description Returns user details for the specified nickname
|
||||
// @Tags users
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param nickname path string true "User nickname"
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.User}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/users/{nickname} [get]
|
||||
func UserGet(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
nickname := utils.Sanitize(vars["nickname"])
|
||||
|
||||
@@ -11,6 +11,19 @@ import (
|
||||
|
||||
var maxLimit = 1000
|
||||
|
||||
// UserList godoc
|
||||
// @Summary List all users
|
||||
// @Description Returns a list of all users with optional limit
|
||||
// @Tags users
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param limit query int false "Maximum number of users to return"
|
||||
// @Success 200 {object} utils.APIResponse{data=[]utils.User}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/users [get]
|
||||
func UserList(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_USERS_READ) != nil {
|
||||
return
|
||||
|
||||
@@ -17,6 +17,18 @@ type LoginRequestJSON struct {
|
||||
Password string `validate:"required,min=8,max=128,containsany=~!@#$%^&*()_+=-{[}]:;"'<>.?/,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"`
|
||||
}
|
||||
|
||||
// UserLogin godoc
|
||||
// @Summary User login
|
||||
// @Description Authenticates a user with nickname and password, sets JWT cookie on success
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequestJSON true "Login credentials"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/login [post]
|
||||
func UserLogin(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
time.Sleep(time.Duration(rand.Float64()*2) * time.Second)
|
||||
|
||||
@@ -6,6 +6,14 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// UserLogout godoc
|
||||
// @Summary User logout
|
||||
// @Description Logs out the current user by clearing authentication cookies
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Router /api/logout [get]
|
||||
func UserLogout(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "GET") {
|
||||
utils.Debug("UserLogout: Logging out user")
|
||||
|
||||
@@ -8,6 +8,17 @@ import (
|
||||
)
|
||||
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get current user info
|
||||
// @Description Returns the profile information of the currently authenticated user
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse{data=utils.User}
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/me [get]
|
||||
func Me(w http.ResponseWriter, req *http.Request) {
|
||||
if (req.Method == "GET") {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
|
||||
|
||||
@@ -14,6 +14,18 @@ type PasswordResetRequestJSON struct {
|
||||
Email string `validate:"required,min=3,max=32,alphanum"`
|
||||
}
|
||||
|
||||
// ResetPassword godoc
|
||||
// @Summary Request a password reset
|
||||
// @Description Sends a password reset email to the user if email is configured
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body PasswordResetRequestJSON true "Nickname and email for password reset"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/password-reset [post]
|
||||
func ResetPassword(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "POST") {
|
||||
if !utils.IsEmailEnabled() {
|
||||
|
||||
@@ -17,6 +17,18 @@ type RegisterRequestJSON struct {
|
||||
RegisterKey string `validate:"required,min=1,max=512,alphanum"`
|
||||
}
|
||||
|
||||
// UserRegister godoc
|
||||
// @Summary Register a new user
|
||||
// @Description Completes user registration using a registration key (invite link)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RegisterRequestJSON true "Registration details including register key"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/register [post]
|
||||
func UserRegister(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "POST") {
|
||||
time.Sleep(time.Duration(rand.Float64()*2)*time.Second)
|
||||
|
||||
@@ -14,6 +14,21 @@ type InviteRequestJSON struct {
|
||||
FormType string
|
||||
}
|
||||
|
||||
// UserResendInviteLink godoc
|
||||
// @Summary Resend invite link
|
||||
// @Description Generates a new registration key and optionally sends an invite or password reset email
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body InviteRequestJSON true "Nickname and form type"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/invite [post]
|
||||
func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "POST") {
|
||||
var request InviteRequestJSON
|
||||
|
||||
@@ -15,6 +15,19 @@ type SudoRequestJSON struct {
|
||||
Password string `validate:"required,min=8,max=128,containsany=~!@#$%^&*()_+=-{[}]:;"'<>.?/,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"`
|
||||
}
|
||||
|
||||
// UserSudo godoc
|
||||
// @Summary Elevate to sudo mode
|
||||
// @Description Re-authenticates the user with their password to grant elevated (sudo) permissions
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body SudoRequestJSON true "Password for sudo elevation"
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 401 {object} utils.HTTPErrorResult
|
||||
// @Failure 405 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/sudo [post]
|
||||
func UserSudo(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
time.Sleep(time.Duration(rand.Float64()*2) * time.Second)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
// APIResponse is the standard success response wrapper used by all endpoints.
|
||||
type APIResponse struct {
|
||||
Status string `json:"status" example:"OK"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// APIResponseMessage is used when only status and message are returned.
|
||||
type APIResponseMessage struct {
|
||||
Status string `json:"status" example:"OK"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// StatusData represents the data returned by the /api/status endpoint.
|
||||
type StatusData struct {
|
||||
HomepageConfig HomepageConfig `json:"homepage"`
|
||||
ThemeConfig ThemeConfig `json:"theme"`
|
||||
CPU string `json:"CPU"`
|
||||
AVX bool `json:"AVX"`
|
||||
LetsEncryptErrors []string `json:"LetsEncryptErrors"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
MonitoringDisabled bool `json:"MonitoringDisabled"`
|
||||
Hostname string `json:"hostname"`
|
||||
Domain bool `json:"domain"`
|
||||
HTTPSCertificateMode string `json:"HTTPSCertificateMode"`
|
||||
NewVersionAvailable string `json:"newVersionAvailable"`
|
||||
NeedsRestart bool `json:"needRestart"`
|
||||
Database bool `json:"database"`
|
||||
Docker bool `json:"docker"`
|
||||
BackupStatus string `json:"backup_status"`
|
||||
Constellation bool `json:"constellation"`
|
||||
}
|
||||
@@ -31,6 +31,16 @@ type Notification struct {
|
||||
Actions []NotificationActions
|
||||
}
|
||||
|
||||
// NotifGet godoc
|
||||
// @Summary Get notifications for the authenticated user
|
||||
// @Tags Notifications
|
||||
// @Produce json
|
||||
// @Param from query string false "Pagination cursor (ObjectID hex string for older notifications)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/notifications [get]
|
||||
func NotifGet(w http.ResponseWriter, req *http.Request) {
|
||||
_from := req.URL.Query().Get("from")
|
||||
from, _ := primitive.ObjectIDFromHex(_from)
|
||||
@@ -102,6 +112,18 @@ func NotifGet(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// MarkAsRead godoc
|
||||
// @Summary Mark notifications as read
|
||||
// @Tags Notifications
|
||||
// @Produce json
|
||||
// @Param ids query string true "Comma-separated list of notification ObjectID hex strings"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} utils.HTTPErrorResult
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Failure 500 {object} utils.HTTPErrorResult
|
||||
// @Router /api/notifications/read [get]
|
||||
func MarkAsRead(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "GET") {
|
||||
if CheckPermissions(w, req, PERM_LOGIN) != nil {
|
||||
|
||||
+13
-13
@@ -207,7 +207,7 @@ type Config struct {
|
||||
|
||||
type CRONConfig struct {
|
||||
Enabled bool
|
||||
Name string
|
||||
Name string `validate:"required"`
|
||||
Crontab string
|
||||
Command string
|
||||
Container string
|
||||
@@ -368,7 +368,7 @@ type EmailConfig struct {
|
||||
}
|
||||
|
||||
type OpenIDClient struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
Secret string `json:"secret"`
|
||||
Redirect string `json:"redirect"`
|
||||
}
|
||||
@@ -411,7 +411,7 @@ type ConstellationTunnel struct {
|
||||
|
||||
type ConstellationDNSEntry struct {
|
||||
Type string
|
||||
Key string
|
||||
Key string `validate:"required"`
|
||||
Value string
|
||||
}
|
||||
|
||||
@@ -437,7 +437,7 @@ type NebulaFirewallRule struct {
|
||||
Port string `yaml:"port"`
|
||||
Proto string `yaml:"proto"`
|
||||
Host string `yaml:"host"`
|
||||
Groups []string `yaml:"groups,omitempty"omitempty"`
|
||||
Groups []string `yaml:"groups,omitempty"`
|
||||
}
|
||||
|
||||
type NebulaConntrackConfig struct {
|
||||
@@ -504,15 +504,15 @@ type NebulaConfig struct {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
DeviceName string `json:"deviceName"validate:"required,min=3,max=32,alphanum",bson:"DeviceName"`
|
||||
Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum",bson:"Nickname"`
|
||||
PublicKey string `json:"publicKey",omitempty,bson:"PublicKey"`
|
||||
PrivateKey string `json:"privateKey",omitempty,bson:"PrivateKey"`
|
||||
IP string `json:"ip",validate:"required,ipv4",bson:"IP"`
|
||||
DeviceName string `json:"deviceName" validate:"required,min=3,max=32,alphanum" bson:"DeviceName"`
|
||||
Nickname string `json:"nickname" validate:"required,min=3,max=32,alphanum" bson:"Nickname"`
|
||||
PublicKey string `json:"publicKey,omitempty" bson:"PublicKey"`
|
||||
PrivateKey string `json:"privateKey,omitempty" bson:"PrivateKey"`
|
||||
IP string `json:"ip" validate:"required,ipv4" bson:"IP"`
|
||||
}
|
||||
|
||||
type Alert struct {
|
||||
Name string
|
||||
Name string `validate:"required"`
|
||||
Enabled bool
|
||||
Period string
|
||||
TrackingMetric string
|
||||
@@ -559,10 +559,10 @@ type BackupConfig struct {
|
||||
}
|
||||
|
||||
type SingleBackupConfig struct {
|
||||
Name string
|
||||
Repository string
|
||||
Name string `validate:"required"`
|
||||
Repository string `validate:"required"`
|
||||
Password string
|
||||
Source string
|
||||
Source string
|
||||
Crontab string
|
||||
CrontabForget string
|
||||
RetentionPolicy string
|
||||
|
||||
+1
-1
@@ -351,7 +351,7 @@ func LoadBaseMainConfig(config Config) {
|
||||
}
|
||||
|
||||
if MainConfig.DockerConfig.DefaultDataPath == "" {
|
||||
MainConfig.DockerConfig.DefaultDataPath = "/usr"
|
||||
MainConfig.DockerConfig.DefaultDataPath = "/cosmos-storage"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user