[skip ci] Go SDK

This commit is contained in:
Yann Stepienik
2026-03-17 15:24:06 +00:00
parent c4a354d444
commit 2816fed4cc
97 changed files with 57006 additions and 78 deletions
+17 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+10471
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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
+19
View File
@@ -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,
};
}
+43
View File
@@ -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,
};
}
+35
View File
@@ -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
View File
@@ -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,
};
}
+4
View File
@@ -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,
};
+44 -1
View File
@@ -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,
};
}
+54
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+365
View File
@@ -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)
}
}
+23
View File
@@ -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
)
+54
View File
@@ -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=
+7
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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": [
+39
View File
@@ -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}"
+5
View File
@@ -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."
+7
View File
@@ -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
View File
@@ -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';
+139
View File
@@ -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
View File
@@ -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",
})
}
+11
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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
+273
View File
@@ -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
View File
@@ -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
+11
View File
@@ -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
+285
View File
@@ -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",
})
}
+14
View File
@@ -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
+14 -3
View File
@@ -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)
+23 -12
View File
@@ -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
+11
View File
@@ -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)
+9
View File
@@ -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" {
+12
View File
@@ -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")
+9
View File
@@ -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" {
+263
View File
@@ -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",
})
}
+9
View File
@@ -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)
+42
View File
@@ -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
+23
View File
@@ -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
+8
View File
@@ -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")
+61
View File
@@ -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
+259
View File
@@ -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
View File
@@ -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
+11
View File
@@ -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
+13 -1
View File
@@ -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
+20
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
+14 -2
View File
@@ -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
+65 -1
View File
@@ -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
+12
View File
@@ -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
+30
View File
@@ -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
+13
View File
@@ -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
+32 -1
View File
@@ -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
+15
View File
@@ -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)
+34
View File
@@ -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"
+29
View File
@@ -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
+7
View File
@@ -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
+10
View File
@@ -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
+27
View File
@@ -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
+244
View File
@@ -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",
})
}
+15
View File
@@ -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
+12
View File
@@ -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
View File
@@ -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" {
+12 -1
View File
@@ -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
+17 -3
View File
@@ -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
View File
@@ -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
+25
View File
@@ -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
+5 -5
View File
@@ -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) {
+8
View File
@@ -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
View File
@@ -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
+61
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+13
View File
@@ -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"])
+15
View File
@@ -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"])
+13
View File
@@ -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"])
+13
View File
@@ -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
+12
View File
@@ -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)
+8
View File
@@ -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")
+11
View File
@@ -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 {
+12
View File
@@ -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() {
+12
View File
@@ -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)
+15
View File
@@ -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
+13
View File
@@ -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)
+33
View File
@@ -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"`
}
+22
View File
@@ -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
View File
@@ -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
View File
@@ -351,7 +351,7 @@ func LoadBaseMainConfig(config Config) {
}
if MainConfig.DockerConfig.DefaultDataPath == "" {
MainConfig.DockerConfig.DefaultDataPath = "/usr"
MainConfig.DockerConfig.DefaultDataPath = "/cosmos-storage"
}
}