Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Larsen
57052d2301 feat: Enhance Navbar with mobile menu functionality and active child detection 2025-05-20 10:18:06 +00:00
1549 changed files with 82527 additions and 76455 deletions

View File

@@ -209,6 +209,22 @@ jobs:
- name: build docker image
run: sudo docker build -f ./Dashboard/Dockerfile .
docker-build-haraka:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build images
- name: build docker image
run: sudo docker build -f ./Haraka/Dockerfile .
docker-build-probe:
runs-on: ubuntu-latest
env:

View File

@@ -31,6 +31,8 @@ jobs:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'typescript', 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]

View File

@@ -290,16 +290,4 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd TestServer && npm install && npm run compile && npm run dep-check
compile-mcp:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd MCP && npm install && npm run compile && npm run dep-check
- run: cd TestServer && npm install && npm run compile && npm run dep-check

View File

@@ -1,74 +0,0 @@
name: OpenAPI Spec Generation
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*'
- 'release'
jobs:
generate-openapi-spec:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{ github.run_number }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Generate OpenAPI specification
run: npm run generate-openapi-spec
- name: Check if OpenAPI spec was generated
run: |
if [ -f "./openapi.json" ]; then
echo "✅ OpenAPI spec file generated successfully"
echo "📄 File size: $(du -h ./openapi.json | cut -f1)"
echo "📊 Spec contains $(jq '.paths | length' ./openapi.json) API paths"
echo "🏷️ API version: $(jq -r '.info.version' ./openapi.json)"
echo "📝 API title: $(jq -r '.info.title' ./openapi.json)"
else
echo "❌ OpenAPI spec file was not generated"
exit 1
fi
- name: Validate OpenAPI spec format
run: |
# Check if the file is valid JSON
if jq empty ./openapi.json; then
echo "✅ OpenAPI spec is valid JSON"
else
echo "❌ OpenAPI spec is not valid JSON"
exit 1
fi
# Check if it has required OpenAPI fields
if jq -e '.openapi and .info and .paths' ./openapi.json > /dev/null; then
echo "✅ OpenAPI spec has required fields"
else
echo "❌ OpenAPI spec missing required fields (openapi, info, paths)"
exit 1
fi
- name: Upload OpenAPI spec as artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: ./openapi.json
retention-days: 30

View File

@@ -68,142 +68,6 @@ jobs:
git commit -m "Helm Chart Release 7.0.${{needs.generate-build-number.outputs.build_number}}"
git push origin master
publish-mcp-server:
runs-on: ubuntu-latest
needs: [generate-build-number, publish-npm-packages]
env:
CI_PIPELINE_ID: ${{ github.run_number }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
permissions:
contents: write # For creating releases
packages: write # For publishing packages
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Determine version
id: version
run: |
VERSION="7.0.${{needs.generate-build-number.outputs.build_number}}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing MCP server version: $VERSION"
- name: Verify MCP server directory
run: |
MCP_DIR="./MCP"
if [ ! -d "$MCP_DIR" ]; then
echo "❌ MCP server directory not found"
exit 1
fi
echo "✅ MCP server directory found"
echo "📊 Source files:"
find "$MCP_DIR" -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | wc -l
echo "📁 Directory structure:"
ls -la "$MCP_DIR"
- name: Setup npm authentication
run: |
# Clean up any existing npm configuration that might cause warnings
rm -f ~/.npmrc
# Create npmrc file with authentication
touch ~/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN" >> ~/.npmrc
echo "//registry.npmjs.org/:email=npm@oneuptime.com" >> ~/.npmrc
echo "✅ npm authentication configured"
- name: Update package version
run: |
cd MCP
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
- name: Install dependencies
run: |
cd MCP
npm update @oneuptime/common
npm install
- name: Build MCP server
run: |
cd MCP
npm run build
- name: Run tests
run: |
cd MCP
npm test || echo "No tests found or tests failed, continuing..."
- name: Verify package before publish
run: |
cd MCP
echo "📦 Package information:"
npm publish --dry-run
echo "📋 Package.json bin configuration:"
cat package.json | grep -A 5 -B 5 '"bin"'
echo "📁 Build directory contents:"
ls -la build/
- name: Publish to npm
run: |
cd MCP
npm publish --access public
echo "✅ Published @oneuptime/mcp-server@${{ steps.version.outputs.version }} to npm"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:release \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:release \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed Docker images to Docker Hub and GitHub Container Registry"
- name: Upload MCP server artifact
uses: actions/upload-artifact@v4
with:
name: mcp-server-${{ steps.version.outputs.version }}
path: ./MCP/
retention-days: 90
nginx-docker-image-deploy:
needs: [generate-build-number]
runs-on: ubuntu-latest
@@ -1053,6 +917,67 @@ jobs:
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
haraka-docker-image-deploy:
needs: [generate-build-number]
runs-on: ubuntu-latest
steps:
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/haraka
ghcr.io/oneuptime/haraka
tags: |
type=raw,value=release,enable=true
type=semver,value=7.0.${{needs.generate-build-number.outputs.build_number}},pattern={{version}},enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate Dockerfile from Dockerfile.tpl
run: npm run prerun
# Build and deploy haraka.
- name: Login to Docker Hub
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
file: ./Haraka/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
admin-dashboard-docker-image-deploy:
needs: [generate-build-number]
runs-on: ubuntu-latest
@@ -1374,6 +1299,8 @@ jobs:
llm-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1457,6 +1384,8 @@ jobs:
docs-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1522,6 +1451,8 @@ jobs:
worker-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1587,6 +1518,8 @@ jobs:
workflow-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1648,71 +1581,13 @@ jobs:
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
publish-terraform-provider:
runs-on: ubuntu-latest
needs: [generate-build-number]
env:
CI_PIPELINE_ID: ${{github.run_number}}
GITHUB_TOKEN: ${{ secrets.SIMLARSEN_GITHUB_PAT }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY: ${{ secrets.TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY }}
permissions:
contents: write # For creating releases
packages: write # For publishing packages
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'latest'
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
install-only: true
- name: Determine version
id: version
run: |
VERSION="7.0.${{needs.generate-build-number.outputs.build_number}}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing Terraform provider version: $VERSION"
- name: Install dependencies
run: |
npm install
if [ -d "Common" ]; then cd Common && npm install && cd ..; fi
if [ -d "Scripts" ]; then cd Scripts && npm install && cd ..; fi
- name: Import GPG key
run: |
echo '${{ secrets.GPG_PRIVATE_KEY }}' > private.key
gpg --import private.key || true
rm private.key
echo "GPG key imported successfully"
gpg --export-secret-keys >~/.gnupg/secring.gpg
echo "GPG key exported successfully"
- name: Generate Terraform provider
run: npm run publish-terraform-provider -- --version "${{ steps.version.outputs.version }}" --github-token "${{ secrets.SIMLARSEN_GITHUB_PAT }}" --github-repo-deploy-key "${{ secrets.TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY }}"
api-reference-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1778,7 +1653,7 @@ jobs:
test-e2e-release-saas:
runs-on: ubuntu-latest
needs: [open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
needs: [open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, haraka-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
@@ -1831,7 +1706,7 @@ jobs:
test-e2e-release-self-hosted:
runs-on: ubuntu-latest
# After all the jobs runs
needs: [open-telemetry-ingest-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, nginx-docker-image-deploy]
needs: [open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, haraka-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, nginx-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
@@ -1949,3 +1824,4 @@ jobs:
prerelease: false
tag_name: 7.0.${{needs.generate-build-number.outputs.build_number}}

View File

@@ -1,97 +0,0 @@
name: Terraform Provider Generation
on:
pull_request:
push:
branches:
- main
- master
- develop
workflow_dispatch: # Allow manual trigger
jobs:
generate-terraform-provider:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{ github.run_number }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Generate Terraform provider
run: npm run generate-terraform-provider
- name: Verify provider generation
run: |
PROVIDER_DIR="./Terraform"
# Check if provider directory was created
if [ ! -d "$PROVIDER_DIR" ]; then
echo "❌ Terraform provider directory not created"
exit 1
fi
echo "✅ Provider directory created: $PROVIDER_DIR"
# Count generated files
GO_FILES=$(find "$PROVIDER_DIR" -name "*.go" | wc -l)
echo "📊 Generated Go files: $GO_FILES"
if [ "$GO_FILES" -eq 0 ]; then
echo "❌ No Go files were generated"
exit 1
fi
# Check for essential files
if [ -f "$PROVIDER_DIR/go.mod" ]; then
echo "✅ Go module file created"
fi
if [ -f "$PROVIDER_DIR/README.md" ]; then
echo "✅ Documentation created"
fi
# Show directory structure for debugging
echo "📁 Provider directory structure:"
ls -la "$PROVIDER_DIR" || true
- name: Test Go build
run: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
- name: Upload Terraform provider as artifact
uses: actions/upload-artifact@v4
with:
name: Terraform
path: ./Terraform/
retention-days: 30

View File

@@ -23,183 +23,10 @@ jobs:
token: ${{secrets.github_token}}
- run: echo "Build number is ${{ steps.buildnumber.outputs.build_number }}"
publish-terraform-provider:
runs-on: ubuntu-latest
needs: [generate-build-number]
env:
CI_PIPELINE_ID: ${{github.run_number}}
GITHUB_TOKEN: ${{ secrets.SIMLARSEN_GITHUB_PAT }}
GPG_PRIVATE_KEY: ${{ secrets.TERRAFORM_GPG_PRIVATE_KEY }}
TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY: ${{ secrets.TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY }}
permissions:
contents: write # For creating releases
packages: write # For publishing packages
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'latest'
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
install-only: true
- name: Determine version
id: version
run: |
VERSION="7.0.${{needs.generate-build-number.outputs.build_number}}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing Terraform provider version: $VERSION"
- name: Install dependencies
run: |
npm install
if [ -d "Common" ]; then cd Common && npm install && cd ..; fi
if [ -d "Scripts" ]; then cd Scripts && npm install && cd ..; fi
- name: Import GPG key
run: |
echo '${{ secrets.TERRAFORM_GPG_PRIVATE_KEY }}' > private.key
gpg --import private.key || true
rm private.key
echo "GPG key imported successfully"
gpg --export-secret-keys >~/.gnupg/secring.gpg
echo "GPG key exported successfully"
- name: Generate Terraform provider
run: npm run publish-terraform-provider -- --version "${{ steps.version.outputs.version }}" --github-token "${{ secrets.SIMLARSEN_GITHUB_PAT }}" --github-repo-deploy-key "${{ secrets.TERRAFORM_PROVIDER_GITHUB_REPO_DEPLOY_KEY }}" --test-release
publish-mcp-server:
runs-on: ubuntu-latest
needs: [generate-build-number]
env:
CI_PIPELINE_ID: ${{ github.run_number }}
permissions:
contents: write # For creating releases
packages: write # For publishing packages
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Determine version
id: version
run: |
VERSION="7.0.${{needs.generate-build-number.outputs.build_number}}-test"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing MCP server version: $VERSION"
- name: Verify MCP server directory
run: |
MCP_DIR="./MCP"
if [ ! -d "$MCP_DIR" ]; then
echo "❌ MCP server directory not found"
exit 1
fi
echo "✅ MCP server directory found"
echo "📊 Source files:"
find "$MCP_DIR" -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | wc -l
echo "📁 Directory structure:"
ls -la "$MCP_DIR"
- name: Update package version
run: |
cd MCP
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
- name: Install dependencies and build
run: |
cd MCP
npm update @oneuptime/common
npm install
npm run build
- name: Run tests
run: |
cd MCP
npm test || echo "No tests found or tests failed, continuing..."
- name: Publish to npm (dry run for test releases)
run: |
cd MCP
npm pack --dry-run
echo "✅ Dry run completed successfully for test release"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images (test)
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:test \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed test Docker images to Docker Hub and GitHub Container Registry"
- name: Upload MCP server artifact
uses: actions/upload-artifact@v4
with:
name: mcp-server-${{ steps.version.outputs.version }}
path: ./MCP/
retention-days: 90
llm-docker-image-deploy:
needs: generate-build-number
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -1147,6 +974,67 @@ jobs:
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
haraka-docker-image-deploy:
needs: generate-build-number
runs-on: ubuntu-latest
steps:
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/haraka
ghcr.io/oneuptime/haraka
tags: |
type=raw,value=test,enable=true
type=semver,value=7.0.${{needs.generate-build-number.outputs.build_number}}-test,pattern={{version}},enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate Dockerfile from Dockerfile.tpl
run: npm run prerun
# Build and deploy haraka.
- name: Login to Docker Hub
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
file: ./Haraka/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
dashboard-docker-image-deploy:
needs: generate-build-number
runs-on: ubuntu-latest
@@ -1708,7 +1596,7 @@ jobs:
test-helm-chart:
runs-on: ubuntu-latest
needs: [infrastructure-agent-deploy, publish-mcp-server, llm-docker-image-deploy, publish-terraform-provider, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
needs: [infrastructure-agent-deploy, llm-docker-image-deploy, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, haraka-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -1,21 +0,0 @@
name: MCP Server Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd MCP && npm install && npm run test

22
.gitignore vendored
View File

@@ -86,6 +86,9 @@ Backups/*.tar
.env
Haraka/dkim/keys/private_base64.txt
Haraka/dkim/keys/public_base64.txt
.eslintcache
HelmChart/Values/*.values.yaml
@@ -109,22 +112,3 @@ App/greenlock/greenlock.d/config.json
App/greenlock/greenlock.d/config.json.bak
Examples/otel-dotnet/bin/Debug/net6.0/Grpc.Core.Api.dll.txt
InfrastructureAgent/oneuptime-infrastructure-agent
# ESLint cache
.eslintcache*
# Terraform generated files
openapi.json
Terraform/**
TerraformTest/**
terraform-provider-example/**
# MCP Server
MCP/build/
MCP/.env
MCP/node_modules
Dashboard/public/sw.js
.claude/settings.local.json

View File

@@ -1,8 +1,6 @@
{
"query": {
"age": {
"_type": "EqualTo",
value: 10
}
"name": "Hello",
// other filters
}
}

View File

@@ -1,11 +0,0 @@
{
"query": {
"labels": {
"_type": "Includes",
"value": [
"aaa00000-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"bbb00000-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
]
}
}
}

View File

@@ -65,8 +65,6 @@ CMD [ "npm", "run", "dev" ]
COPY ./APIReference /usr/src/app
# Bundle app source
RUN npm run compile
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

View File

@@ -1,7 +1,6 @@
import AuthenticationServiceHandler from "./Service/Authentication";
import DataTypeServiceHandler from "./Service/DataType";
import ErrorServiceHandler from "./Service/Errors";
import OpenAPIServiceHandler from "./Service/OpenAPI";
import IntroductionServiceHandler from "./Service/Introduction";
import ModelServiceHandler from "./Service/Model";
import PageNotFoundServiceHandler from "./Service/PageNotFound";
@@ -66,8 +65,6 @@ const APIReferenceFeatureSet: FeatureSet = {
return ErrorServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "introduction") {
return IntroductionServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "openapi") {
return OpenAPIServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "status") {
return StatusServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "data-types") {

View File

@@ -88,16 +88,6 @@ export default class ServiceHandler {
},
);
pageData.includesCode = await LocalCache.getOrSetString(
"data-type",
"includes",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/Includes.md`,
);
},
);
pageData.lessThanOrNullCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",

View File

@@ -77,11 +77,6 @@ export default class ServiceHandler {
continue;
}
if (tableColumns[key].hideColumnInDocumentation) {
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
}

View File

@@ -1,44 +0,0 @@
import {
Host,
HttpProtocol,
IsBillingEnabled,
} from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import URL from "Common/Types/API/URL";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler {
// Handles the HTTP response for a given request
public static async executeResponse(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
let pageTitle: string = "";
let pageDescription: string = "";
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: any = {
hostUrl: new URL(HttpProtocol, Host).toString(),
};
// Set the default page title and description
pageTitle = "OneUptime OpenAPI Specification";
pageDescription =
"Learn more about the OpenAPI specification for OneUptime";
// Render the response using the given view and data
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,
pageData: pageData,
});
}
}

View File

@@ -1,16 +1,17 @@
{
"name": "@oneuptime/api-reference",
"name": "@oneuptime/app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@oneuptime/api-reference",
"name": "@oneuptime/app",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"Common": "file:../Common",
"ejs": "^3.1.9",
"handlebars": "^4.7.8",
"ts-node": "^10.9.1"
},
"devDependencies": {
@@ -25,9 +26,8 @@
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.2",
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@clickhouse/client": "^0.2.10",
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
@@ -55,19 +55,18 @@
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^8.3.4",
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"cookie-parser": "^1.4.7",
"Common": "file:../Common",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"express": "^4.19.2",
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
@@ -75,6 +74,7 @@
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"marked": "^12.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
@@ -82,7 +82,6 @@
"nodemailer": "^6.9.10",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
@@ -105,7 +104,6 @@
"redis-semaphore": "^5.5.1",
"reflect-metadata": "^0.2.2",
"remark-gfm": "^3.0.1",
"slackify-markdown": "^4.4.0",
"slugify": "^1.6.5",
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
@@ -115,11 +113,9 @@
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"universal-cookie": "^4.0.4",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"uuid": "^8.3.2"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -133,6 +129,7 @@
"@types/jest": "^28.1.4",
"@types/json2csv": "^5.0.3",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.202",
"@types/node": "^17.0.45",
"@types/node-cron": "^3.0.7",
"@types/nodemailer": "^6.4.7",
@@ -146,7 +143,6 @@
"jest-environment-jsdom": "^29.7.0",
"jest-mock-extended": "^3.0.5",
"react-test-renderer": "^18.2.0",
"sass": "^1.89.2",
"ts-jest": "^28.0.5"
}
},
@@ -2382,6 +2378,26 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -3429,6 +3445,14 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -3441,6 +3465,11 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -3932,7 +3961,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4215,6 +4243,18 @@
"node": ">=14.17"
}
},
"node_modules/uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -4300,6 +4340,11 @@
"node": ">= 8"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@@ -32,7 +32,7 @@
<p>You can use OneUptime API Key on Request Header when you're making a request. You can use <code
class="rounded p-0.5 px-1 text-sm text-gray-500 bg-gray-100 border-2 border-gray-200">Authorization</code>
header with your API Key when you make a request.</p>
<%- include('../partials/code', {title: "Example request with API Key" , requestUrl: "" , requestType: "" , code: "curl --header \"ApiKey: {secret-api-key}\" https://oneuptime.com/api/\<path\>" }) -%>
<%- include('../partials/code', {title: "Example request with API Key" , requestUrl: "" , requestType: "" , code: "curl --header \"ApiKey: {secret-api-key}\" --header \"ProjectID: {project-id}\" https://oneuptime.com/api/\<path\>" }) -%>
<p class="text-sm">Please don't commit your OneUptime API Key to GitHub, or on any other source control
project. Please regenerate a new API Key, if your API Key is committed by mistake.</p>
</article>

View File

@@ -115,7 +115,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of an Equal To Query</p>
<p>Here is an example of Equal To Query</p>
</dd>
</dl>
</li>
@@ -153,7 +153,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a Not Equal To Query</p>
<p>Here is an example of Not Equal To Query</p>
</dd>
</dl>
</li>
@@ -191,7 +191,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of an is null query</p>
<p>Here is an example of is null query</p>
</dd>
</dl>
</li>
@@ -229,7 +229,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of an is not null query</p>
<p>Here is an example of is not null query</p>
</dd>
</dl>
</li>
@@ -266,7 +266,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a greater than query</p>
<p>Here is an example of greater than query</p>
</dd>
</dl>
</li>
@@ -304,7 +304,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a greater than or equal query</p>
<p>Here is an example of greater or equal than query</p>
</dd>
</dl>
</li>
@@ -342,7 +342,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a less than query</p>
<p>Here is an example of less than query</p>
</dd>
</dl>
</li>
@@ -380,7 +380,7 @@
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a less than or equal query</p>
<p>Here is an example of less or equal than query</p>
</dd>
</dl>
</li>
@@ -395,44 +395,5 @@
</div>
</div>
<h3 id="example-using-cursors" class="scroll-mt-24">
Inlcudes
</h3>
<div class="grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>
Includes will get objects that match any of the values in the array. It is used to
filter objects that have a field that matches any of the values in the array. For example, if you
want to get all objects that have a label with ID `aaa00000-aaaa-aaaa-aaaa-aaaaaaaaaaaa` or
`bbb00000-bbbb-bbbb-bbbb-bbbbbbbbbbbb`, you can use the `includes` query type.
</p>
<div class="my-6">
<ul role="list"
class="m-0 max-w-[calc(theme(maxWidth.lg)-theme(spacing.8))] list-none divide-y divide-zinc-900/5 p-0 ">
<li class="m-0 px-0 py-4 first:pt-0 last:pb-0">
<dl class="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dt class="sr-only">Query</dt>
<dd><code class="inline-code">query</code></dd>
<dt class="sr-only">Type</dt>
<dd class="font-mono text-xs text-zinc-400 ">Query</dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Here is an example of a less than or equal query</p>
</dd>
</dl>
</li>
</ul>
</div>
</div>
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<%- include('../partials/code', {title: "Example Not EqualTo Request Body" , requestUrl: "" , requestType: "" ,
code: pageData.includesCode }) -%>
</div>
</div>
</article>
</main>

View File

@@ -1,53 +0,0 @@
<main class="py-16">
<article class="prose ">
<h1>OneUptime OpenAPI Specification</h1>
<p class="lead">In this guide, we will look at how to import and work with the OneUptime OpenAPI specification. The OpenAPI spec provides a comprehensive reference for all available API endpoints.</p>
<p>The OneUptime API follows the OpenAPI 3.0 specification, which provides a standardized way to describe REST APIs. You can import our OpenAPI spec into various tools like Swagger Editor, Postman, or other API documentation tools to explore and test our endpoints.</p>
<h2 id="importing-spec" class="scroll-mt-24">
Importing the OpenAPI Spec
</h2>
<div class="grid grid-cols-1 items-start gap-x-16 gap-y-10">
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>To import the OneUptime OpenAPI specification into Swagger Editor, follow these simple steps:</p>
<div class="my-6">
<ol class="list-decimal pl-6">
<li class="mb-2">Open <a href="https://editor.swagger.io" target="_blank" class="text-blue-600 hover:text-blue-800">editor.swagger.io</a> in your web browser</li>
<li class="mb-2">Click on <strong>File</strong> in the menu bar</li>
<li class="mb-2">Select <strong>Import URL</strong> from the dropdown menu</li>
<li class="mb-2">Enter the URL: <code class="inline-code"><%= pageData.hostUrl %>api/openapi/spec</code></li>
<li class="mb-2">Click <strong>Import</strong> to load the specification</li>
</ol>
</div>
<h2 id="spec-url" class="scroll-mt-24">
Specification URL
</h2>
<div class="my-6">
<ul role="list"
class="m-0 list-none divide-y divide-zinc-900/5 p-0 ">
<li class="m-0 px-0 py-4 first:pt-0 last:pb-0">
<dl class="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dt class="sr-only">Endpoint</dt>
<dd><code class="inline-code"><%= pageData.hostUrl %>api/openapi/spec</code></dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>Complete OpenAPI 3.0 specification for the OneUptime API including all endpoints, request/response schemas, and authentication methods.</p>
</dd>
</dl>
</li>
</ul>
</div>
<h2 id="benefits" class="scroll-mt-24">
Benefits of Using the OpenAPI Spec
</h2>
<div class="my-6">
<ul class="list-disc pl-6">
<li class="mb-2"><strong>Interactive Documentation:</strong> Test API endpoints directly in Swagger Editor</li>
<li class="mb-2"><strong>Code Generation:</strong> Generate client SDKs in multiple programming languages</li>
<li class="mb-2"><strong>Validation:</strong> Ensure your requests match the expected schema</li>
<li class="mb-2"><strong>Import to Tools:</strong> Use with Postman, Insomnia, or other API testing tools</li>
</ul>
</div>
</div>
</div>
</article>
</main>

View File

@@ -13,7 +13,7 @@
</h2>
<div class="grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>In this example, we request the list of monitors. As a result, we get a list of three monitors and can
<p>In this example, we request the list fo monitors. As a result, we get a list of three monitors and can
tell by the <code class="inline-code">count</code> attribute that we have reached the end of the
result set</p>
<h2 id="example-using-cursors" class="scroll-mt-24">

View File

@@ -146,9 +146,6 @@
with us on Slack</a></li>
<li><a class="text-sm leading-5 text-zinc-600 transition hover:text-zinc-900 "
href="/support">Support</a></li>
<li><a class="text-sm leading-5 text-zinc-600 transition hover:text-zinc-900 "
href="/reference/openapi" type="_blank">OpenAPI Spec</a></li>
<li><a class="text-sm leading-5 text-zinc-600 transition hover:text-zinc-900 "
href="https://github.com/oneuptime/oneuptime">GitHub</a></li>
</ul>
@@ -235,10 +232,6 @@
class="flex justify-between gap-2 py-1 pr-3 text-sm transition pl-4 text-zinc-600 hover:text-zinc-900 "
href="/reference/errors"><span class="truncate">Errors</span></a></li>
<li class="relative" style="transform: none; transform-origin: 50% 50% 0px;"><a
class="flex justify-between gap-2 py-1 pr-3 text-sm transition pl-4 text-zinc-600 hover:text-zinc-900 "
href="/reference/openapi"><span class="truncate">OpenAPI Spec</span></a></li>
</ul>
</div>

View File

@@ -69,11 +69,12 @@ RUN npm install
# - 3003: accounts
EXPOSE 3003
RUN npm i -D webpack-cli
{{ if eq .Env.ENVIRONMENT "development" }}
RUN mkdir /usr/src/app/dev-env
RUN touch /usr/src/app/dev-env/.env
RUN npm i -D webpack-dev-server
#Run the app
CMD [ "npm", "run", "dev" ]
@@ -83,8 +84,6 @@ COPY ./Accounts /usr/src/app
# Bundle app source
RUN npm run build
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

View File

@@ -35,7 +35,7 @@ See the section about [deployment](https://facebook.github.io/create-react-app/d
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies ( Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.

View File

@@ -1,12 +0,0 @@
const { createConfig, build, watch } = require('Common/UI/esbuild-config');
const config = createConfig({
serviceName: 'Accounts',
publicPath: '/accounts/dist/',
});
if (process.argv.includes('--watch')) {
watch(config, 'Accounts');
} else {
build(config, 'Accounts');
}

View File

@@ -4,11 +4,13 @@
"ignore": [
"./public/**",
"./public/dist/**",
"./dev-env/*",
"./dev-env/.env",
"./build/*",
"./build/**",
"./build/dist/*",
"./build/dist/**",
"../Common/Server/**"
],
"exec": "npm run dev-build && npm run start"
"exec": "printenv > /usr/src/app/dev-env/.env && echo 'HOST=localhost' >> /usr/src/app/dev-env/.env && echo 'USE_HTTPS=false' >> /usr/src/app/dev-env/.env && npm run dev-build && npm run start"
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,9 @@
"version": "0.1.0",
"private": false,
"scripts": {
"dev-build": "NODE_ENV=development node esbuild.config.js",
"dev-build": "webpack build --mode=development",
"dev": "npx nodemon",
"build": "NODE_ENV=production node esbuild.config.js",
"analyze": "analyze=true NODE_ENV=production node esbuild.config.js",
"build": "webpack build --mode=production",
"test": "",
"compile": "tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
@@ -29,11 +28,16 @@
},
"dependencies": {
"Common": "file:../Common",
"css-loader": "^6.11.0",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"file-loader": "^6.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"sass-loader": "^13.3.3",
"style-loader": "^3.3.4",
"ts-loader": "^9.5.1",
"use-async-effect": "^2.2.7"
},
"devDependencies": {
@@ -42,7 +46,7 @@
"@types/react-dom": "^18.0.4",
"@types/react-router-dom": "^5.3.3",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1"
"ts-node": "^10.9.1",
"webpack": "^5.76.0"
}
}

View File

@@ -1,4 +1,12 @@
import React, { ReactElement, lazy, Suspense } from "react";
import ForgotPasswordPage from "./Pages/ForgotPassword";
import LoginPage from "./Pages/Login";
import LoginWithSSO from "./Pages/LoginWithSSO";
import NotFound from "./Pages/NotFound";
import RegisterPage from "./Pages/Register";
import ResetPasswordPage from "./Pages/ResetPassword";
import VerifyEmail from "./Pages/VerifyEmail";
import Navigation from "Common/UI/Utils/Navigation";
import React, { ReactElement } from "react";
import {
Route,
Routes,
@@ -6,38 +14,6 @@ import {
useNavigate,
useParams,
} from "react-router-dom";
import Navigation from "Common/UI/Utils/Navigation";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
// Lazy load page components
const ForbiddenPage: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/Forbidden");
});
const ForgotPasswordPage: React.LazyExoticComponent<() => JSX.Element> = lazy(
() => {
return import("./Pages/ForgotPassword");
},
);
const LoginPage: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/Login");
});
const LoginWithSSO: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/LoginWithSSO");
});
const NotFound: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/NotFound");
});
const RegisterPage: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/Register");
});
const ResetPasswordPage: React.LazyExoticComponent<() => JSX.Element> = lazy(
() => {
return import("./Pages/ResetPassword");
},
);
const VerifyEmail: React.LazyExoticComponent<() => JSX.Element> = lazy(() => {
return import("./Pages/VerifyEmail");
});
function App(): ReactElement {
Navigation.setNavigateHook(useNavigate());
@@ -46,29 +22,24 @@ function App(): ReactElement {
return (
<div className="m-auto h-screen">
<Suspense fallback={<PageLoader isVisible={true} />}>
<Routes>
<Route path="/accounts" element={<LoginPage />} />
<Route path="/accounts/login" element={<LoginPage />} />
<Route path="/accounts/forbidden" element={<ForbiddenPage />} />
<Route path="/accounts/sso" element={<LoginWithSSO />} />
<Route
path="/accounts/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route
path="/accounts/reset-password/:token"
element={<ResetPasswordPage />}
/>
<Route path="/accounts/register" element={<RegisterPage />} />
<Route
path="/accounts/verify-email/:token"
element={<VerifyEmail />}
/>
{/* 👇️ only match this when no other routes match */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
<Routes>
<Route path="/accounts" element={<LoginPage />} />
<Route path="/accounts/login" element={<LoginPage />} />
<Route path="/accounts/sso" element={<LoginWithSSO />} />
<Route
path="/accounts/forgot-password"
element={<ForgotPasswordPage />}
/>
<Route
path="/accounts/reset-password/:token"
element={<ResetPasswordPage />}
/>
<Route path="/accounts/register" element={<RegisterPage />} />
<Route path="/accounts/verify-email/:token" element={<VerifyEmail />} />
{/* 👇️ only match this when no other routes match */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}

View File

@@ -1,18 +0,0 @@
import React from "react";
const ForbiddenPage: () => JSX.Element = () => {
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Forbidden
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
You do not have permission to access this page.
</p>
</div>
</div>
);
};
export default ForbiddenPage;

View File

@@ -56,7 +56,6 @@ const ForgotPassword: () => JSX.Element = () => {
title: "Email",
fieldType: FormFieldSchemaType.Email,
required: true,
disableSpellCheck: true,
},
]}
onSuccess={() => {

View File

@@ -122,7 +122,6 @@ const LoginPage: () => JSX.Element = () => {
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
disableSpellCheck: true,
},
{
field: {
@@ -140,7 +139,6 @@ const LoginPage: () => JSX.Element = () => {
openLinkInNewTab: false,
},
dataTestId: "password",
disableSpellCheck: true,
},
]}
createOrUpdateApiUrl={apiUrl}

View File

@@ -196,7 +196,6 @@ const LoginPage: () => JSX.Element = () => {
required: true,
title: "Email",
dataTestId: "email",
disableSpellCheck: true,
},
]}
maxPrimaryButtonWidth={true}

View File

@@ -104,7 +104,6 @@ const RegisterPage: () => JSX.Element = () => {
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
disableSpellCheck: true,
},
{
field: {
@@ -115,7 +114,6 @@ const RegisterPage: () => JSX.Element = () => {
required: true,
title: "Full Name",
dataTestId: "name",
disableSpellCheck: true,
},
];
@@ -130,7 +128,6 @@ const RegisterPage: () => JSX.Element = () => {
required: true,
title: "Company Name",
dataTestId: "companyName",
disableSpellCheck: true,
},
]);
@@ -162,7 +159,6 @@ const RegisterPage: () => JSX.Element = () => {
title: "Password",
required: true,
dataTestId: "password",
disableSpellCheck: true,
},
{
field: {
@@ -179,7 +175,6 @@ const RegisterPage: () => JSX.Element = () => {
required: true,
showEvenIfPermissionDoesNotExist: true,
dataTestId: "confirmPassword",
disableSpellCheck: true,
},
]);

View File

@@ -68,7 +68,6 @@ const RegisterPage: () => JSX.Element = () => {
title: "New Password",
required: true,
showEvenIfPermissionDoesNotExist: true,
disableSpellCheck: true,
},
{
field: {
@@ -84,7 +83,6 @@ const RegisterPage: () => JSX.Element = () => {
overrideFieldKey: "confirmPassword",
required: true,
showEvenIfPermissionDoesNotExist: true,
disableSpellCheck: true,
},
]}
createOrUpdateApiUrl={apiUrl}

View File

@@ -106,7 +106,7 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/accounts/dist/Index.js"></script>
<script src="/accounts/dist/bundle.js"></script>
<script>
tailwind.config = {
theme: {

View File

@@ -0,0 +1,70 @@
require("ts-loader");
require("file-loader");
require("style-loader");
require("css-loader");
require("sass-loader");
const path = require("path");
const webpack = require("webpack");
const dotenv = require("dotenv");
require('ejs');
const readEnvFile = (pathToFile) => {
const parsed = dotenv.config({ path: pathToFile }).parsed;
const env = {};
for (const key in parsed) {
env[key] = JSON.stringify(parsed[key]);
}
return env;
};
module.exports = {
entry: "./src/Index.tsx",
mode: "development",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "public", "dist"),
publicPath: "/accounts/dist/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss"],
alias: {
react: path.resolve("./node_modules/react"),
},
},
externals: {
"react-native-sqlite-storage": "react-native-sqlite-storage",
},
plugins: [
new webpack.DefinePlugin({
process: {
env: {
...readEnvFile("/usr/src/app/dev-env/.env"),
},
},
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
},
{
test: /\.s[ac]ss$/i,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: "file-loader",
},
],
},
devtool: "eval-source-map",
};

View File

@@ -67,20 +67,19 @@ RUN npm install
# - 3158: AdminDashboard
EXPOSE 3158
RUN npm i -D webpack-cli
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app
RUN mkdir /usr/src/app/dev-env
RUN touch /usr/src/app/dev-env/.env
RUN npm i -D webpack-dev-server
CMD [ "npm", "run", "dev" ]
{{ else }}
# Copy app source
COPY ./AdminDashboard /usr/src/app
# Bundle app source
RUN npm run build
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

View File

@@ -35,7 +35,7 @@ See the section about [deployment](https://facebook.github.io/create-react-app/d
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies ( Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.

View File

@@ -1,12 +0,0 @@
const { createConfig, build, watch } = require('Common/UI/esbuild-config');
const config = createConfig({
serviceName: 'AdminDashboard',
publicPath: '/admin/dist/',
});
if (process.argv.includes('--watch')) {
watch(config, 'AdminDashboard');
} else {
build(config, 'AdminDashboard');
}

View File

@@ -4,11 +4,13 @@
"ignore": [
"./public/**",
"./public/dist/**",
"./dev-env/*",
"./dev-env/.env",
"./build/*",
"./build/**",
"./build/dist/*",
"./build/dist/**",
"../Common/Server/**"
],
"exec": " npm run dev-build && npm run start"
"exec": "printenv > /usr/src/app/dev-env/.env && echo 'HOST=localhost' >> /usr/src/app/dev-env/.env && echo 'USE_HTTPS=false' >> /usr/src/app/dev-env/.env && npm run dev-build && npm run start"
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,20 @@
"private": false,
"dependencies": {
"Common": "file:../Common",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"file-loader": "^6.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
"react-router-dom": "^6.23.1",
"style-loader": "^3.3.4"
},
"scripts": {
"dev-build": "NODE_ENV=development node esbuild.config.js",
"dev-build": "webpack build --mode=development",
"dev": "npx nodemon",
"build": "NODE_ENV=production node esbuild.config.js",
"analyze": "analyze=true NODE_ENV=production node esbuild.config.js",
"build": "webpack build --mode=production",
"test": "react-app-rewired test",
"eject": "echo 'esbuild does not require eject'",
"eject": "webpack eject",
"compile": "tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"start": "node --require ts-node/register Serve.ts",
@@ -41,8 +42,13 @@
"@types/react": "^18.2.38",
"@types/react-dom": "^18.0.4",
"@types/react-router-dom": "^5.3.3",
"css-loader": "^6.8.1",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1"
"react-app-rewired": "^2.2.1",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"webpack": "^5.76.0"
}
}

View File

@@ -67,7 +67,7 @@ const DashboardFooter: () => JSX.Element = () => {
return (
<>
<Footer
className="bg-white px-8"
className="bg-white h-16 inset-x-0 bottom-0 px-8"
copyright="HackerBay, Inc."
links={[
{

View File

@@ -17,9 +17,7 @@ const Logo: FunctionComponent<ComponentProps> = (
<Image
className="block h-8 w-auto"
onClick={() => {
if (props.onClick) {
props.onClick();
}
props.onClick && props.onClick();
}}
imageUrl={Route.fromString(`${OneUptimeLogo}`)}
alt={"OneUptime"}

View File

@@ -2,33 +2,36 @@ import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import NavBar, { NavItem } from "Common/UI/Components/Navbar/NavBar";
import NavBar from "Common/UI/Components/Navbar/NavBar";
import NavBarItem from "Common/UI/Components/Navbar/NavBarItem";
import React, { FunctionComponent, ReactElement } from "react";
const DashboardNavbar: FunctionComponent = (): ReactElement => {
// Build the navigation items
const navItems: NavItem[] = [
{
id: "users-nav-bar-item",
title: "Users",
icon: IconProp.User,
route: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
},
{
id: "projects-nav-bar-item",
title: "Projects",
icon: IconProp.Folder,
route: RouteUtil.populateRouteParams(RouteMap[PageMap.PROJECTS] as Route),
},
{
id: "settings-nav-bar-item",
title: "Settings",
icon: IconProp.Settings,
route: RouteUtil.populateRouteParams(RouteMap[PageMap.SETTINGS] as Route),
},
];
return (
<NavBar>
<NavBarItem
title="Users"
icon={IconProp.User}
route={RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route)}
></NavBarItem>
return <NavBar items={navItems} />;
<NavBarItem
title="Projects"
icon={IconProp.Folder}
route={RouteUtil.populateRouteParams(
RouteMap[PageMap.PROJECTS] as Route,
)}
></NavBarItem>
<NavBarItem
title="Settings"
icon={IconProp.Settings}
route={RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route,
)}
></NavBarItem>
</NavBar>
);
};
export default DashboardNavbar;

View File

@@ -16,7 +16,7 @@ const Logout: FunctionComponent = (): ReactElement => {
const logout: PromiseVoidFunction = async (): Promise<void> => {
UiAnalytics.logout();
UserUtil.logout();
await UserUtil.logout();
Navigation.navigate(ACCOUNTS_URL);
};

View File

@@ -241,7 +241,6 @@ const Projects: FunctionComponent = (): ReactElement => {
},
title: "Created At",
type: FieldType.DateTime,
hideOnMobile: true,
},
]}
userPreferencesKey="admin-projects-table"

View File

@@ -21,7 +21,7 @@ import React, { FunctionComponent, ReactElement, useEffect } from "react";
const Settings: FunctionComponent = (): ReactElement => {
const [emailServerType, setemailServerType] = React.useState<EmailServerType>(
EmailServerType.CustomSMTP,
EmailServerType.Internal,
);
const [isLoading, setIsLoading] = React.useState<boolean>(true);
@@ -43,7 +43,7 @@ const Settings: FunctionComponent = (): ReactElement => {
if (globalConfig) {
setemailServerType(
globalConfig.emailServerType || EmailServerType.CustomSMTP,
globalConfig.emailServerType || EmailServerType.Internal,
);
}
@@ -106,7 +106,6 @@ const Settings: FunctionComponent = (): ReactElement => {
title: "Admin Notification Email",
fieldType: FormFieldSchemaType.Email,
required: false,
disableSpellCheck: true,
},
]}
modelDetailProps={{
@@ -127,7 +126,7 @@ const Settings: FunctionComponent = (): ReactElement => {
/>
<CardModelDetail
name="Email Server Settings"
name="Internal SMTP Settings"
cardProps={{
title: "Email Server Settings",
description:
@@ -172,7 +171,7 @@ const Settings: FunctionComponent = (): ReactElement => {
cardProps={{
title: "Custom Email and SMTP Settings",
description:
"Please configure your SMTP server here to send emails.",
"If you have not enabled Internal SMTP server to send emails. Please configure your SMTP server here.",
}}
isEditable={true}
editButtonText="Edit SMTP Config"
@@ -200,7 +199,6 @@ const Settings: FunctionComponent = (): ReactElement => {
fieldType: FormFieldSchemaType.Hostname,
required: true,
placeholder: "smtp.server.com",
disableSpellCheck: true,
},
{
field: {
@@ -231,7 +229,6 @@ const Settings: FunctionComponent = (): ReactElement => {
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "emailuser",
disableSpellCheck: true,
},
{
field: {
@@ -242,7 +239,6 @@ const Settings: FunctionComponent = (): ReactElement => {
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
placeholder: "Password",
disableSpellCheck: true,
},
{
field: {
@@ -255,7 +251,6 @@ const Settings: FunctionComponent = (): ReactElement => {
description:
"Email used to log in to this SMTP Server. This is also the email your customers will see. ",
placeholder: "email@company.com",
disableSpellCheck: true,
},
{
field: {
@@ -268,7 +263,6 @@ const Settings: FunctionComponent = (): ReactElement => {
description:
"This is the display name your team and customers see, when they receive emails from OneUptime.",
placeholder: "Company, Inc.",
disableSpellCheck: true,
},
]}
modelDetailProps={{

View File

@@ -54,7 +54,6 @@ const Settings: FunctionComponent = (): ReactElement => {
title="Need help with setting up Global Probes?"
description="Here is a guide which will help you get set up"
link={Route.fromString("/docs/probe/custom-probe")}
hideOnMobile={true}
/>
<ModelTable<Probe>
@@ -153,7 +152,7 @@ const Settings: FunctionComponent = (): ReactElement => {
description: true,
},
title: "Description",
type: FieldType.LongText,
type: FieldType.Text,
},
]}
columns={[
@@ -174,8 +173,7 @@ const Settings: FunctionComponent = (): ReactElement => {
},
noValueMessage: "-",
title: "Description",
type: FieldType.LongText,
hideOnMobile: true,
type: FieldType.Text,
},
{
field: {
@@ -183,7 +181,6 @@ const Settings: FunctionComponent = (): ReactElement => {
},
title: "Status",
type: FieldType.Text,
getElement: (item: Probe): ReactElement => {
if (
item &&

View File

@@ -48,7 +48,6 @@ const Users: FunctionComponent = (): ReactElement => {
fieldType: FormFieldSchemaType.Email,
required: true,
placeholder: "email@company.com",
disableSpellCheck: true,
},
{
field: {
@@ -58,7 +57,6 @@ const Users: FunctionComponent = (): ReactElement => {
fieldType: FormFieldSchemaType.Password,
required: true,
placeholder: "Password",
disableSpellCheck: true,
},
{
field: {
@@ -116,7 +114,6 @@ const Users: FunctionComponent = (): ReactElement => {
},
title: "Email Verified",
type: FieldType.Boolean,
hideOnMobile: true,
},
{
field: {
@@ -124,7 +121,6 @@ const Users: FunctionComponent = (): ReactElement => {
},
title: "Created At",
type: FieldType.DateTime,
hideOnMobile: true,
},
]}
/>

View File

@@ -100,7 +100,7 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/admin/dist/Index.js"></script>
<script src="/admin/dist/bundle.js"></script>
<script>
tailwind.config = {
theme: {

View File

@@ -0,0 +1,70 @@
require("ts-loader");
require("file-loader");
require("style-loader");
require("css-loader");
require("sass-loader");
require('ejs');
const path = require("path");
const webpack = require("webpack");
const dotenv = require("dotenv");
const readEnvFile = (pathToFile) => {
const parsed = dotenv.config({ path: pathToFile }).parsed;
const env = {};
for (const key in parsed) {
env[key] = JSON.stringify(parsed[key]);
}
return env;
};
module.exports = {
entry: "./src/Index.tsx",
mode: "development",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "public", "dist"),
publicPath: "/admin/dist/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss"],
alias: {
react: path.resolve("./node_modules/react"),
},
},
externals: {
"react-native-sqlite-storage": "react-native-sqlite-storage",
},
plugins: [
new webpack.DefinePlugin({
process: {
env: {
...readEnvFile("/usr/src/app/dev-env/.env"),
},
},
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
},
{
test: /\.s[ac]ss$/i,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: "file-loader",
},
],
},
devtool: "eval-source-map",
};

View File

@@ -65,8 +65,6 @@ CMD [ "npm", "run", "dev" ]
COPY ./App /usr/src/app
# Bundle app source
RUN npm run compile
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

View File

@@ -29,7 +29,6 @@ import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
import UserPushAPI from "Common/Server/API/UserPushAPI";
import ApiKeyPermissionService, {
Service as ApiKeyPermissionServiceType,
} from "Common/Server/Services/ApiKeyPermissionService";
@@ -282,9 +281,6 @@ import ShortLinkService, {
import SmsLogService, {
Service as SmsLogServiceType,
} from "Common/Server/Services/SmsLogService";
import PushNotificationLogService, {
Service as PushNotificationLogServiceType,
} from "Common/Server/Services/PushNotificationLogService";
import SpanService, {
SpanService as SpanServiceType,
} from "Common/Server/Services/SpanService";
@@ -382,8 +378,6 @@ import Span from "Common/Models/AnalyticsModels/Span";
import ApiKey from "Common/Models/DatabaseModels/ApiKey";
import ApiKeyPermission from "Common/Models/DatabaseModels/ApiKeyPermission";
import CallLog from "Common/Models/DatabaseModels/CallLog";
import PushNotificationLog from "Common/Models/DatabaseModels/PushNotificationLog";
import WorkspaceNotificationLog from "Common/Models/DatabaseModels/WorkspaceNotificationLog";
import Domain from "Common/Models/DatabaseModels/Domain";
import EmailLog from "Common/Models/DatabaseModels/EmailLog";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
@@ -485,9 +479,6 @@ import TelemetryAttribute from "Common/Models/AnalyticsModels/TelemetryAttribute
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
import TelemetyException from "Common/Models/DatabaseModels/TelemetryException";
import CopilotActionTypePriority from "Common/Models/DatabaseModels/CopilotActionTypePriority";
import WorkspaceNotificationLogService, {
Service as WorkspaceNotificationLogServiceType,
} from "Common/Server/Services/WorkspaceNotificationLogService";
// scheduled maintenance template
import ScheduledMaintenanceTemplate from "Common/Models/DatabaseModels/ScheduledMaintenanceTemplate";
@@ -585,27 +576,6 @@ import OnCallDutyPolicyTimeLogService, {
Service as OnCallDutyPolicyTimeLogServiceType,
} from "Common/Server/Services/OnCallDutyPolicyTimeLogService";
// statu spage announcement templates
import StatusPageAnnouncementTemplate from "Common/Models/DatabaseModels/StatusPageAnnouncementTemplate";
import StatusPageAnnouncementTemplateService, {
Service as StatusPageAnnouncementTemplateServiceType,
} from "Common/Server/Services/StatusPageAnnouncementTemplateService";
// ProjectSCIM
import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM";
import ProjectSCIMService, {
Service as ProjectSCIMServiceType,
} from "Common/Server/Services/ProjectSCIMService";
// StatusPageSCIM
import StatusPageSCIM from "Common/Models/DatabaseModels/StatusPageSCIM";
import StatusPageSCIMService, {
Service as StatusPageSCIMServiceType,
} from "Common/Server/Services/StatusPageSCIMService";
// Open API Spec
import OpenAPI from "Common/Server/API/OpenAPI";
const BaseAPIFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp();
@@ -620,8 +590,6 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, OpenAPI.getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAnalyticsAPI<MonitorLog, MonitorLogServiceType>(
@@ -638,36 +606,6 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
// Project SCIM
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<ProjectSCIM, ProjectSCIMServiceType>(
ProjectSCIM,
ProjectSCIMService,
).getRouter(),
);
// Status Page SCIM
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<StatusPageSCIM, StatusPageSCIMServiceType>(
StatusPageSCIM,
StatusPageSCIMService,
).getRouter(),
);
// status page announcement templates
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
StatusPageAnnouncementTemplate,
StatusPageAnnouncementTemplateServiceType
>(
StatusPageAnnouncementTemplate,
StatusPageAnnouncementTemplateService,
).getRouter(),
);
// OnCallDutyPolicyTimeLogService
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
@@ -1539,22 +1477,6 @@ const BaseAPIFeatureSet: FeatureSet = {
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
PushNotificationLog,
PushNotificationLogService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
WorkspaceNotificationLog,
WorkspaceNotificationLogServiceType
>(WorkspaceNotificationLog, WorkspaceNotificationLogService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<EmailLog, EmailLogServiceType>(
@@ -1663,7 +1585,6 @@ const BaseAPIFeatureSet: FeatureSet = {
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());
app.use(

View File

@@ -1,722 +0,0 @@
import SCIMMiddleware from "Common/Server/Middleware/SCIMAuthorization";
import UserService from "Common/Server/Services/UserService";
import TeamMemberService from "Common/Server/Services/TeamMemberService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
OneUptimeRequest,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import logger from "Common/Server/Utils/Logger";
import ObjectID from "Common/Types/ObjectID";
import Email from "Common/Types/Email";
import Name from "Common/Types/Name";
import { JSONObject } from "Common/Types/JSON";
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM";
import BadRequestException from "Common/Types/Exception/BadRequestException";
import NotFoundException from "Common/Types/Exception/NotFoundException";
import OneUptimeDate from "Common/Types/Date";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Query from "Common/Types/BaseDatabase/Query";
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
import User from "Common/Models/DatabaseModels/User";
import {
parseNameFromSCIM,
formatUserForSCIM,
generateServiceProviderConfig,
generateUsersListResponse,
parseSCIMQueryParams,
logSCIMOperation,
} from "../Utils/SCIMUtils";
import { DocsClientUrl } from "Common/Server/EnvironmentConfig";
const router: ExpressRouter = Express.getRouter();
const handleUserTeamOperations: (
operation: "add" | "remove",
projectId: ObjectID,
userId: ObjectID,
scimConfig: ProjectSCIM,
) => Promise<void> = async (
operation: "add" | "remove",
projectId: ObjectID,
userId: ObjectID,
scimConfig: ProjectSCIM,
): Promise<void> => {
const teamsIds: Array<ObjectID> =
scimConfig.teams?.map((team: any) => {
return team.id;
}) || [];
if (teamsIds.length === 0) {
logger.debug(`SCIM Team operations - no teams configured for SCIM`);
return;
}
if (operation === "add") {
logger.debug(
`SCIM Team operations - adding user to ${teamsIds.length} configured teams`,
);
for (const team of scimConfig.teams || []) {
const existingMember: TeamMember | null =
await TeamMemberService.findOneBy({
query: {
projectId: projectId,
userId: userId,
teamId: team.id!,
},
select: { _id: true },
props: { isRoot: true },
});
if (!existingMember) {
logger.debug(`SCIM Team operations - adding user to team: ${team.id}`);
const teamMember: TeamMember = new TeamMember();
teamMember.projectId = projectId;
teamMember.userId = userId;
teamMember.teamId = team.id!;
teamMember.hasAcceptedInvitation = true;
teamMember.invitationAcceptedAt = OneUptimeDate.getCurrentDate();
await TeamMemberService.create({
data: teamMember,
props: {
isRoot: true,
ignoreHooks: true,
},
});
} else {
logger.debug(
`SCIM Team operations - user already member of team: ${team.id}`,
);
}
}
} else if (operation === "remove") {
logger.debug(
`SCIM Team operations - removing user from ${teamsIds.length} configured teams`,
);
await TeamMemberService.deleteBy({
query: {
projectId: projectId,
userId: userId,
teamId: QueryHelper.any(teamsIds),
},
skip: 0,
limit: LIMIT_PER_PROJECT,
props: { isRoot: true },
});
}
};
// SCIM Service Provider Configuration - GET /scim/v2/ServiceProviderConfig
router.get(
"/scim/v2/:projectScimId/ServiceProviderConfig",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logSCIMOperation(
"ServiceProviderConfig",
"project",
req.params["projectScimId"]!,
);
const serviceProviderConfig: JSONObject = generateServiceProviderConfig(
req,
req.params["projectScimId"]!,
"project",
DocsClientUrl.toString() + "/identity/scim",
);
logger.debug(
"Project SCIM ServiceProviderConfig response prepared successfully",
);
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Basic Users endpoint - GET /scim/v2/Users
router.get(
"/scim/v2/:projectScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logSCIMOperation("Users list", "project", req.params["projectScimId"]!);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
// Parse query parameters
const { startIndex, count } = parseSCIMQueryParams(req);
const filter: string = req.query["filter"] as string;
logSCIMOperation(
"Users list",
"project",
req.params["projectScimId"]!,
`startIndex: ${startIndex}, count: ${count}, filter: ${filter || "none"}`,
);
// Build query for team members in this project
const query: Query<ProjectUser> = {
projectId: projectId,
};
// Handle SCIM filter for userName
if (filter) {
const emailMatch: RegExpMatchArray | null = filter.match(
/userName eq "([^"]+)"/i,
);
if (emailMatch) {
const email: string = emailMatch[1]!;
logSCIMOperation(
"Users list",
"project",
req.params["projectScimId"]!,
`filter by email: ${email}`,
);
if (email) {
const user: User | null = await UserService.findOneBy({
query: { email: new Email(email) },
select: { _id: true },
props: { isRoot: true },
});
if (user && user.id) {
query.userId = user.id;
logSCIMOperation(
"Users list",
"project",
req.params["projectScimId"]!,
`found user with id: ${user.id}`,
);
} else {
logSCIMOperation(
"Users list",
"project",
req.params["projectScimId"]!,
`user not found for email: ${email}`,
);
return Response.sendJsonObjectResponse(
req,
res,
generateUsersListResponse([], startIndex, 0),
);
}
}
}
}
logSCIMOperation(
"Users list",
"project",
req.params["projectScimId"]!,
`query built for projectId: ${projectId}`,
);
// Get team members
const teamMembers: Array<TeamMember> = await TeamMemberService.findBy({
query: query,
limit: LIMIT_MAX,
skip: 0,
props: { isRoot: true },
select: {
userId: true,
user: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
},
});
// now get unique users.
const usersInProjects: Array<JSONObject> = teamMembers
.filter((tm: TeamMember) => {
return tm.user && tm.user.id;
})
.map((tm: TeamMember) => {
return formatUserForSCIM(
tm.user!,
req,
req.params["projectScimId"]!,
"project",
);
});
// remove duplicates
const uniqueUserIds: Set<string> = new Set<string>();
const users: Array<JSONObject> = usersInProjects.filter(
(user: JSONObject) => {
if (uniqueUserIds.has(user["id"]?.toString() || "")) {
return false;
}
uniqueUserIds.add(user["id"]?.toString() || "");
return true;
},
);
// now paginate the results
const paginatedUsers: Array<JSONObject> = users.slice(
(startIndex - 1) * count,
startIndex * count,
);
logger.debug(`SCIM Users response prepared with ${users.length} users`);
return Response.sendJsonObjectResponse(
req,
res,
generateUsersListResponse(paginatedUsers, startIndex, users.length),
);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Get Individual User - GET /scim/v2/Users/{id}
router.get(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const userId: string = req.params["userId"]!;
logger.debug(
`SCIM Get user - projectId: ${projectId}, userId: ${userId}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and is part of the project
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
query: {
projectId: projectId,
userId: new ObjectID(userId),
},
select: {
userId: true,
user: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
},
props: { isRoot: true },
});
if (!projectUser || !projectUser.user) {
logger.debug(
`SCIM Get user - user not found or not part of project for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this project",
);
}
logger.debug(`SCIM Get user - found user: ${projectUser.user.id}`);
const user: JSONObject = formatUserForSCIM(
projectUser.user,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Update User - PUT /scim/v2/Users/{id}
router.put(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
logger.debug(
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
);
logger.debug(
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and is part of the project
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
query: {
projectId: projectId,
userId: new ObjectID(userId),
},
select: {
userId: true,
user: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
},
props: { isRoot: true },
});
if (!projectUser || !projectUser.user) {
logger.debug(
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this project",
);
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const name: string = parseNameFromSCIM(scimUser);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
);
// Handle user deactivation by removing from teams
if (active === false) {
logger.debug(
`SCIM Update user - user marked as inactive, removing from teams`,
);
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
await handleUserTeamOperations(
"remove",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully removed from teams due to deactivation`,
);
}
// Handle user activation by adding to teams
if (active === true) {
logger.debug(
`SCIM Update user - user marked as active, adding to teams`,
);
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
await handleUserTeamOperations(
"add",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Update user - user successfully added to teams due to activation`,
);
}
if (email || name) {
const updateData: any = {};
if (email) {
updateData.email = new Email(email);
}
if (name) {
updateData.name = new Name(name);
}
logger.debug(
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await UserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(`SCIM Update user - user updated successfully`);
// Fetch updated user
const updatedUser: User | null = await UserService.findOneById({
id: new ObjectID(userId),
select: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
}
}
logger.debug(
`SCIM Update user - no updates made, returning existing user`,
);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
projectUser.user,
req,
req.params["projectScimId"]!,
"project",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Groups endpoint - GET /scim/v2/Groups
router.get(
"/scim/v2/:projectScimId/Groups",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
logger.debug(
`SCIM Groups - found ${scimConfig.teams?.length || 0} configured teams`,
);
// Return configured teams as groups
const groups: JSONObject[] = (scimConfig.teams || []).map((team: any) => {
return {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
id: team.id?.toString(),
displayName: team.name?.toString(),
members: [],
meta: {
resourceType: "Group",
location: `${req.protocol}://${req.get("host")}/scim/v2/${req.params["projectScimId"]}/Groups/${team.id?.toString()}`,
},
};
});
return Response.sendJsonObjectResponse(req, res, {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: groups.length,
startIndex: 1,
itemsPerPage: groups.length,
Resources: groups,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Create User - POST /scim/v2/Users
router.post(
"/scim/v2/:projectScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`SCIM Create user request for projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
if (!scimConfig.autoProvisionUsers) {
throw new BadRequestException(
"Auto-provisioning is disabled for this project",
);
}
const scimUser: JSONObject = req.body;
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const name: string = parseNameFromSCIM(scimUser);
logger.debug(`SCIM Create user - email: ${email}, name: ${name}`);
if (!email) {
throw new BadRequestException("userName or email is required");
}
// Check if user already exists
let user: User | null = await UserService.findOneBy({
query: { email: new Email(email) },
select: {
_id: true,
email: true,
name: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
// Create user if doesn't exist
if (!user) {
logger.debug(
`SCIM Create user - creating new user for email: ${email}`,
);
user = await UserService.createByEmail({
email: new Email(email),
name: name ? new Name(name) : new Name("Unknown"),
isEmailVerified: true,
generateRandomPassword: true,
props: { isRoot: true },
});
} else {
logger.debug(
`SCIM Create user - user already exists with id: ${user.id}`,
);
}
// Add user to default teams if configured
if (scimConfig.teams && scimConfig.teams.length > 0) {
logger.debug(
`SCIM Create user - adding user to ${scimConfig.teams.length} configured teams`,
);
await handleUserTeamOperations("add", projectId, user.id!, scimConfig);
}
const createdUser: JSONObject = formatUserForSCIM(
user,
req,
req.params["projectScimId"]!,
"project",
);
logger.debug(
`SCIM Create user - returning created user with id: ${user.id}`,
);
res.status(201);
return Response.sendJsonObjectResponse(req, res, createdUser);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Delete User - DELETE /scim/v2/Users/{id}
router.delete(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
const userId: string = req.params["userId"]!;
if (!scimConfig.autoDeprovisionUsers) {
logger.debug("SCIM Delete user - auto-deprovisioning is disabled");
throw new BadRequestException(
"Auto-deprovisioning is disabled for this project",
);
}
if (!userId) {
throw new BadRequestException("User ID is required");
}
logger.debug(
`SCIM Delete user - removing user from all teams in project: ${projectId}`,
);
// Remove user from teams the SCIM configured
if (!scimConfig.teams || scimConfig.teams.length === 0) {
logger.debug("SCIM Delete user - no teams configured for SCIM");
throw new BadRequestException("No teams configured for SCIM");
}
await handleUserTeamOperations(
"remove",
projectId,
new ObjectID(userId),
scimConfig,
);
logger.debug(
`SCIM Delete user - user successfully deprovisioned from project`,
);
res.status(204);
return Response.sendJsonObjectResponse(req, res, {
message: "User deprovisioned",
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
export default router;

View File

@@ -168,7 +168,6 @@ router.post(
},
{
projectId: statusPage.projectId!,
statusPageId: statusPage.id!,
},
).catch((err: Error) => {
logger.error(err);
@@ -307,7 +306,6 @@ router.post(
},
{
projectId: statusPage.projectId!,
statusPageId: statusPage.id!,
},
).catch((err: Error) => {
logger.error(err);

View File

@@ -1,536 +0,0 @@
import SCIMMiddleware from "Common/Server/Middleware/SCIMAuthorization";
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
OneUptimeRequest,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import logger from "Common/Server/Utils/Logger";
import ObjectID from "Common/Types/ObjectID";
import Email from "Common/Types/Email";
import { JSONObject } from "Common/Types/JSON";
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
import StatusPageSCIM from "Common/Models/DatabaseModels/StatusPageSCIM";
import BadRequestException from "Common/Types/Exception/BadRequestException";
import NotFoundException from "Common/Types/Exception/NotFoundException";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import {
formatUserForSCIM,
generateServiceProviderConfig,
logSCIMOperation,
} from "../Utils/SCIMUtils";
import Text from "Common/Types/Text";
import HashedString from "Common/Types/HashedString";
const router: ExpressRouter = Express.getRouter();
// SCIM Service Provider Configuration - GET /status-page-scim/v2/ServiceProviderConfig
router.get(
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logSCIMOperation(
"ServiceProviderConfig",
"status-page",
req.params["statusPageScimId"]!,
);
const serviceProviderConfig: JSONObject = generateServiceProviderConfig(
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Status Page Users endpoint - GET /status-page-scim/v2/Users
router.get(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
// Parse query parameters
const startIndex: number =
parseInt(req.query["startIndex"] as string) || 1;
const count: number = Math.min(
parseInt(req.query["count"] as string) || 100,
LIMIT_PER_PROJECT,
);
logger.debug(
`Status Page SCIM Users - statusPageId: ${statusPageId}, startIndex: ${startIndex}, count: ${count}`,
);
// Get all private users for this status page
const statusPageUsers: Array<StatusPagePrivateUser> =
await StatusPagePrivateUserService.findBy({
query: {
statusPageId: statusPageId,
},
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Users - found ${statusPageUsers.length} users`,
);
// Format users for SCIM
const users: Array<JSONObject> = statusPageUsers.map(
(user: StatusPagePrivateUser) => {
return formatUserForSCIM(
user,
req,
req.params["statusPageScimId"]!,
"status-page",
);
},
);
// Paginate the results
const paginatedUsers: Array<JSONObject> = users.slice(
(startIndex - 1) * count,
startIndex * count,
);
logger.debug(
`Status Page SCIM Users response prepared with ${users.length} users`,
);
return Response.sendJsonObjectResponse(req, res, {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: users.length,
startIndex: startIndex,
itemsPerPage: paginatedUsers.length,
Resources: paginatedUsers,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Get Individual Status Page User - GET /status-page-scim/v2/Users/{id}
router.get(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const userId: string = req.params["userId"]!;
logger.debug(
`Status Page SCIM Get user - statusPageId: ${statusPageId}, userId: ${userId}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and belongs to this status page
const statusPageUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
_id: new ObjectID(userId),
},
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (!statusPageUser) {
logger.debug(
`Status Page SCIM Get user - user not found for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this status page",
);
}
const user: JSONObject = formatUserForSCIM(
statusPageUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
logger.debug(
`Status Page SCIM Get user - returning user with id: ${statusPageUser.id}`,
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Create Status Page User - POST /status-page-scim/v2/Users
router.post(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const scimConfig: StatusPageSCIM = bearerData[
"scimConfig"
] as StatusPageSCIM;
if (!scimConfig.autoProvisionUsers) {
throw new BadRequestException(
"Auto-provisioning is disabled for this status page",
);
}
const scimUser: JSONObject = req.body;
logger.debug(
`Status Page SCIM Create user - statusPageId: ${statusPageId}`,
);
logger.debug(
`Request body for Status Page SCIM Create user: ${JSON.stringify(scimUser, null, 2)}`,
);
// Extract user data from SCIM payload
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
if (!email) {
throw new BadRequestException("Email is required for user creation");
}
logger.debug(`Status Page SCIM Create user - email: ${email}`);
// Check if user already exists for this status page
let user: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
email: new Email(email),
},
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (!user) {
logger.debug(
`Status Page SCIM Create user - creating new user with email: ${email}`,
);
const privateUser: StatusPagePrivateUser = new StatusPagePrivateUser();
privateUser.statusPageId = statusPageId;
privateUser.email = new Email(email);
privateUser.password = new HashedString(Text.generateRandomText(32));
privateUser.projectId = bearerData["projectId"] as ObjectID;
// Create new status page private user
user = await StatusPagePrivateUserService.create({
data: privateUser as any,
props: { isRoot: true },
});
} else {
logger.debug(
`Status Page SCIM Create user - user already exists with id: ${user.id}`,
);
}
const createdUser: JSONObject = formatUserForSCIM(
user,
req,
req.params["statusPageScimId"]!,
"status-page",
);
logger.debug(
`Status Page SCIM Create user - returning created user with id: ${user.id}`,
);
res.status(201);
return Response.sendJsonObjectResponse(req, res, createdUser);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Update Status Page User - PUT /status-page-scim/v2/Users/{id}
router.put(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const userId: string = req.params["userId"]!;
const scimUser: JSONObject = req.body;
logger.debug(
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
);
logger.debug(
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and belongs to this status page
const statusPageUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
_id: new ObjectID(userId),
},
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (!statusPageUser) {
logger.debug(
`Status Page SCIM Update user - user not found for userId: ${userId}`,
);
throw new NotFoundException(
"User not found or not part of this status page",
);
}
// Update user information
const email: string =
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
const active: boolean = scimUser["active"] as boolean;
logger.debug(
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
);
// Handle user deactivation by deleting from status page
if (active === false) {
logger.debug(
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
);
const scimConfig: StatusPageSCIM = bearerData[
"scimConfig"
] as StatusPageSCIM;
if (scimConfig.autoDeprovisionUsers) {
await StatusPagePrivateUserService.deleteOneById({
id: new ObjectID(userId),
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Update user - user removed from status page`,
);
// Return empty response for deleted user
return Response.sendJsonObjectResponse(req, res, {});
}
}
// Prepare update data
const updateData: {
email?: Email;
} = {};
if (email && email !== statusPageUser.email?.toString()) {
updateData.email = new Email(email);
}
// Only update if there are changes
if (Object.keys(updateData).length > 0) {
logger.debug(
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
);
await StatusPagePrivateUserService.updateOneById({
id: new ObjectID(userId),
data: updateData,
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Update user - user updated successfully`,
);
// Fetch updated user
const updatedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: new ObjectID(userId),
select: {
_id: true,
email: true,
createdAt: true,
updatedAt: true,
},
props: { isRoot: true },
});
if (updatedUser) {
const user: JSONObject = formatUserForSCIM(
updatedUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
}
}
logger.debug(
`Status Page SCIM Update user - no updates made, returning existing user`,
);
// If no updates were made, return the existing user
const user: JSONObject = formatUserForSCIM(
statusPageUser,
req,
req.params["statusPageScimId"]!,
"status-page",
);
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
// Delete Status Page User - DELETE /status-page-scim/v2/Users/{id}
router.delete(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
);
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
const bearerData: JSONObject =
oneuptimeRequest.bearerTokenData as JSONObject;
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
const scimConfig: StatusPageSCIM = bearerData[
"scimConfig"
] as StatusPageSCIM;
const userId: string = req.params["userId"]!;
if (!scimConfig.autoDeprovisionUsers) {
throw new BadRequestException(
"Auto-deprovisioning is disabled for this status page",
);
}
logger.debug(
`Status Page SCIM Delete user - statusPageId: ${statusPageId}, userId: ${userId}`,
);
if (!userId) {
throw new BadRequestException("User ID is required");
}
// Check if user exists and belongs to this status page
const statusPageUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: {
statusPageId: statusPageId,
_id: new ObjectID(userId),
},
select: {
_id: true,
},
props: { isRoot: true },
});
if (!statusPageUser) {
logger.debug(
`Status Page SCIM Delete user - user not found for userId: ${userId}`,
);
// SCIM spec says to return 404 for non-existent resources
throw new NotFoundException("User not found");
}
// Delete the user from status page
await StatusPagePrivateUserService.deleteOneById({
id: new ObjectID(userId),
props: { isRoot: true },
});
logger.debug(
`Status Page SCIM Delete user - user deleted successfully for userId: ${userId}`,
);
// Return 204 No Content for successful deletion
res.status(204);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
}
},
);
export default router;

View File

@@ -1,10 +1,8 @@
import AuthenticationAPI from "./API/Authentication";
import ResellerAPI from "./API/Reseller";
import SsoAPI from "./API/SSO";
import SCIMAPI from "./API/SCIM";
import StatusPageAuthenticationAPI from "./API/StatusPageAuthentication";
import StatusPageSsoAPI from "./API/StatusPageSSO";
import StatusPageSCIMAPI from "./API/StatusPageSCIM";
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import "ejs";
@@ -21,10 +19,6 @@ const IdentityFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}`, "/"], SsoAPI);
app.use([`/${APP_NAME}`, "/"], SCIMAPI);
app.use([`/${APP_NAME}`, "/"], StatusPageSCIMAPI);
app.use([`/${APP_NAME}`, "/"], StatusPageSsoAPI);
app.use(

View File

@@ -1,264 +0,0 @@
import { ExpressRequest } from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import { JSONObject } from "Common/Types/JSON";
import Email from "Common/Types/Email";
import Name from "Common/Types/Name";
import ObjectID from "Common/Types/ObjectID";
/**
* Shared SCIM utility functions for both Project SCIM and Status Page SCIM
*/
// Base interface for SCIM user-like objects - compatible with User model
export interface SCIMUser {
id?: ObjectID | null;
email?: Email;
name?: Name | string;
createdAt?: Date;
updatedAt?: Date;
}
/**
* Parse name information from SCIM user payload
*/
export const parseNameFromSCIM: (scimUser: JSONObject) => string = (
scimUser: JSONObject,
): string => {
logger.debug(
`SCIM - Parsing name from SCIM user: ${JSON.stringify(scimUser, null, 2)}`,
);
const givenName: string =
((scimUser["name"] as JSONObject)?.["givenName"] as string) || "";
const familyName: string =
((scimUser["name"] as JSONObject)?.["familyName"] as string) || "";
const formattedName: string = (scimUser["name"] as JSONObject)?.[
"formatted"
] as string;
// Construct full name: prefer formatted, then combine given+family, then fallback to displayName
if (formattedName) {
return formattedName;
} else if (givenName || familyName) {
return `${givenName} ${familyName}`.trim();
} else if (scimUser["displayName"]) {
return scimUser["displayName"] as string;
}
return "";
};
/**
* Parse full name into SCIM name format
*/
export const parseNameToSCIMFormat: (fullName: string) => {
givenName: string;
familyName: string;
formatted: string;
} = (
fullName: string,
): { givenName: string; familyName: string; formatted: string } => {
const nameParts: string[] = fullName.trim().split(/\s+/);
const givenName: string = nameParts[0] || "";
const familyName: string = nameParts.slice(1).join(" ") || "";
return {
givenName,
familyName,
formatted: fullName,
};
};
/**
* Format user object for SCIM response
*/
export const formatUserForSCIM: (
user: SCIMUser,
req: ExpressRequest,
scimId: string,
scimType: "project" | "status-page",
) => JSONObject = (
user: SCIMUser,
req: ExpressRequest,
scimId: string,
scimType: "project" | "status-page",
): JSONObject => {
const baseUrl: string = `${req.protocol}://${req.get("host")}`;
const userName: string = user.email?.toString() || "";
const fullName: string =
user.name?.toString() || userName.split("@")[0] || "Unknown User";
const nameData: { givenName: string; familyName: string; formatted: string } =
parseNameToSCIMFormat(fullName);
// Determine the correct endpoint path based on SCIM type
const endpointPath: string =
scimType === "project"
? `/scim/v2/${scimId}/Users/${user.id?.toString()}`
: `/status-page-scim/v2/${scimId}/Users/${user.id?.toString()}`;
return {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: user.id?.toString(),
userName: userName,
displayName: nameData.formatted,
name: {
formatted: nameData.formatted,
familyName: nameData.familyName,
givenName: nameData.givenName,
},
emails: [
{
value: userName,
type: "work",
primary: true,
},
],
active: true,
meta: {
resourceType: "User",
created: user.createdAt?.toISOString(),
lastModified: user.updatedAt?.toISOString(),
location: `${baseUrl}${endpointPath}`,
},
};
};
/**
* Extract email from SCIM user payload
*/
export const extractEmailFromSCIM: (scimUser: JSONObject) => string = (
scimUser: JSONObject,
): string => {
return (
(scimUser["userName"] as string) ||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string) ||
""
);
};
/**
* Extract active status from SCIM user payload
*/
export const extractActiveFromSCIM: (scimUser: JSONObject) => boolean = (
scimUser: JSONObject,
): boolean => {
return scimUser["active"] !== false; // Default to true if not specified
};
/**
* Generate SCIM ServiceProviderConfig response
*/
export const generateServiceProviderConfig: (
req: ExpressRequest,
scimId: string,
scimType: "project" | "status-page",
documentationUrl?: string,
) => JSONObject = (
req: ExpressRequest,
scimId: string,
scimType: "project" | "status-page",
documentationUrl: string = "https://oneuptime.com/docs/identity/scim",
): JSONObject => {
const baseUrl: string = `${req.protocol}://${req.get("host")}`;
const endpointPath: string =
scimType === "project"
? `/scim/v2/${scimId}`
: `/status-page-scim/v2/${scimId}`;
return {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
documentationUri: documentationUrl,
patch: {
supported: true,
},
bulk: {
supported: true,
maxOperations: 1000,
maxPayloadSize: 1048576,
},
filter: {
supported: true,
maxResults: 200,
},
changePassword: {
supported: false,
},
sort: {
supported: true,
},
etag: {
supported: false,
},
authenticationSchemes: [
{
type: "httpbearer",
name: "HTTP Bearer",
description: "Authentication scheme using HTTP Bearer Token",
primary: true,
},
],
meta: {
location: `${baseUrl}${endpointPath}/ServiceProviderConfig`,
resourceType: "ServiceProviderConfig",
created: "2023-01-01T00:00:00Z",
lastModified: "2023-01-01T00:00:00Z",
},
};
};
/**
* Generate SCIM ListResponse for users
*/
export const generateUsersListResponse: (
users: JSONObject[],
startIndex: number,
totalResults: number,
) => JSONObject = (
users: JSONObject[],
startIndex: number,
totalResults: number,
): JSONObject => {
return {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: totalResults,
startIndex: startIndex,
itemsPerPage: users.length,
Resources: users,
};
};
/**
* Parse query parameters for SCIM list requests
*/
export const parseSCIMQueryParams: (req: ExpressRequest) => {
startIndex: number;
count: number;
} = (req: ExpressRequest): { startIndex: number; count: number } => {
const startIndex: number = parseInt(req.query["startIndex"] as string) || 1;
const count: number = Math.min(
parseInt(req.query["count"] as string) || 100,
200, // SCIM recommended max
);
return { startIndex, count };
};
/**
* Log SCIM operation with consistent format
*/
export const logSCIMOperation: (
operation: string,
scimType: "project" | "status-page",
scimId: string,
details?: string,
) => void = (
operation: string,
scimType: "project" | "status-page",
scimId: string,
details?: string,
): void => {
const logPrefix: string =
scimType === "project" ? "Project SCIM" : "Status Page SCIM";
const message: string = `${logPrefix} ${operation} - scimId: ${scimId}${details ? `, ${details}` : ""}`;
logger.debug(message);
};

View File

@@ -31,22 +31,6 @@ router.post(
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);

View File

@@ -43,43 +43,6 @@ router.post(
emailServer: mailServer,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: body["incidentId"]
? new ObjectID(body["incidentId"].toString())
: undefined,
alertId: body["alertId"]
? new ObjectID(body["alertId"].toString())
: undefined,
scheduledMaintenanceId: body["scheduledMaintenanceId"]
? new ObjectID(body["scheduledMaintenanceId"].toString())
: undefined,
statusPageId: body["statusPageId"]
? new ObjectID(body["statusPageId"].toString())
: undefined,
statusPageAnnouncementId: body["statusPageAnnouncementId"]
? new ObjectID(body["statusPageAnnouncementId"].toString())
: undefined,
userId: body["userId"]
? new ObjectID(body["userId"].toString())
: undefined,
onCallPolicyId: body["onCallPolicyId"]
? new ObjectID(body["onCallPolicyId"].toString())
: undefined,
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
: undefined,
onCallDutyPolicyExecutionLogTimelineId: body[
"onCallDutyPolicyExecutionLogTimelineId"
]
? new ObjectID(
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
)
: undefined,
onCallScheduleId: body["onCallScheduleId"]
? new ObjectID(body["onCallScheduleId"].toString())
: undefined,
teamId: body["teamId"]
? new ObjectID(body["teamId"].toString())
: undefined,
});
return Response.sendEmptySuccessResponse(req, res);

View File

@@ -1,65 +0,0 @@
import PushService from "../Services/PushNotificationService";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import ObjectID from "Common/Types/ObjectID";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from "Common/Types/JSONFunctions";
const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
// Support both new devices format and legacy deviceTokens/deviceNames format
let devices: Array<{ token: string; name?: string }> = [];
if (body["devices"]) {
// New format: devices as array of objects
devices = body["devices"] as Array<{ token: string; name?: string }>;
} else {
throw new Error("Invalid request format: 'devices' array is required.");
}
await PushService.send(
{
devices,
deviceType: (body["deviceType"] as any) || "web",
message: body["message"] as any,
},
{
projectId: (body["projectId"] as ObjectID) || undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
},
);
return Response.sendEmptySuccessResponse(req, res);
},
);
export default router;

View File

@@ -30,22 +30,6 @@ router.post(
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);

View File

@@ -1,8 +1,10 @@
import Hostname from "Common/Types/API/Hostname";
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import Email from "Common/Types/Email";
import EmailServer from "Common/Types/Email/EmailServer";
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import Port from "Common/Types/Port";
import { AdminDashboardClientURL } from "Common/Server/EnvironmentConfig";
import GlobalConfigService from "Common/Server/Services/GlobalConfigService";
import GlobalConfig, {
@@ -10,6 +12,24 @@ import GlobalConfig, {
} from "Common/Models/DatabaseModels/GlobalConfig";
import Phone from "Common/Types/Phone";
export const InternalSmtpPassword: string =
process.env["INTERNAL_SMTP_PASSWORD"] || "";
export const InternalSmtpHost: Hostname = new Hostname(
process.env["INTERNAL_SMTP_HOST"] || "haraka",
);
export const InternalSmtpPort: Port = new Port(2525);
export const InternalSmtpSecure: boolean = false;
export const InternalSmtpEmail: Email = new Email(
process.env["INTERNAL_SMTP_EMAIL"] || "noreply@oneuptime.com",
);
export const InternalSmtpFromName: string =
process.env["INTERNAL_SMTP_FROM_NAME"] || "OneUptime";
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
@@ -112,10 +132,10 @@ export const getEmailServerType: GetEmailServerTypeFunction =
});
if (!globalConfig) {
return EmailServerType.CustomSMTP;
return EmailServerType.Internal;
}
return globalConfig.emailServerType || EmailServerType.CustomSMTP;
return globalConfig.emailServerType || EmailServerType.Internal;
};
export interface SendGridConfig {

View File

@@ -2,7 +2,6 @@ import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
import SmsAPI from "./API/SMS";
import PushNotificationAPI from "./API/PushNotification";
import SMTPConfigAPI from "./API/SMTPConfig";
import "./Utils/Handlebars";
import FeatureSet from "Common/Server/Types/FeatureSet";
@@ -16,7 +15,6 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
},

View File

@@ -28,36 +28,6 @@ import Twilio from "twilio";
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import Phone from "Common/Types/Phone";
/**
* Extracts the main sayMessage values from a CallRequest's data array for call summary.
* Excludes acknowledgment responses, error messages, and other system messages.
* @param callRequest The call request containing data array with various objects
* @returns A string containing main call content messages separated by newlines
*/
function extractSayMessagesFromCallRequest(callRequest: CallRequest): string {
const sayMessages: string[] = [];
if (callRequest.data && Array.isArray(callRequest.data)) {
for (const item of callRequest.data) {
// Check if the item is a Say object with sayMessage
if ((item as Say).sayMessage) {
sayMessages.push((item as Say).sayMessage);
}
// Check if the item is a GatherInput with introMessage
if ((item as GatherInput).introMessage) {
sayMessages.push((item as GatherInput).introMessage);
}
// NOTE: Excluding noInputMessage and onInputCallRequest messages from summary
// as they contain system responses like "Good bye", "Invalid input", "You have acknowledged"
// which should not be included in the call summary according to user requirements
}
}
return sayMessages.length > 0
? sayMessages.join(" ")
: "No message content found";
}
export default class CallService {
public static async makeCall(
callRequest: CallRequest,
@@ -66,18 +36,6 @@ export default class CallService {
isSensitive?: boolean; // if true, message will not be logged
userOnCallLogTimelineId?: ObjectID | undefined; // user notification log timeline id
customTwilioConfig?: TwilioConfig | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
userId?: ObjectID | undefined;
// On-call policy related fields
onCallPolicyId?: ObjectID | undefined;
onCallPolicyEscalationRuleId?: ObjectID | undefined;
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
onCallScheduleId?: ObjectID | undefined;
teamId?: ObjectID | undefined;
},
): Promise<void> {
let callError: Error | null = null;
@@ -124,58 +82,14 @@ export default class CallService {
callLog.fromNumber = fromNumber;
callLog.callData =
options && options.isSensitive
? ({ message: "This call is sensitive and is not logged" } as any)
: ({
message: extractSayMessagesFromCallRequest(callRequest),
} as any);
? { message: "This call is sensitive and is not logged" }
: JSON.parse(JSON.stringify(callRequest));
callLog.callCostInUSDCents = 0;
if (options.projectId) {
callLog.projectId = options.projectId;
}
if (options.incidentId) {
callLog.incidentId = options.incidentId;
}
if (options.alertId) {
callLog.alertId = options.alertId;
}
if (options.scheduledMaintenanceId) {
callLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}
if (options.statusPageId) {
callLog.statusPageId = options.statusPageId;
}
if (options.statusPageAnnouncementId) {
callLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
}
if (options.userId) {
callLog.userId = options.userId;
}
if (options.teamId) {
callLog.teamId = options.teamId;
}
// Set OnCall-related fields
if (options.onCallPolicyId) {
callLog.onCallDutyPolicyId = options.onCallPolicyId;
}
if (options.onCallPolicyEscalationRuleId) {
callLog.onCallDutyPolicyEscalationRuleId =
options.onCallPolicyEscalationRuleId;
}
if (options.onCallScheduleId) {
callLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
}
let project: Project | null = null;
// make sure project has enough balance.

View File

@@ -1,4 +1,10 @@
import {
InternalSmtpEmail,
InternalSmtpFromName,
InternalSmtpHost,
InternalSmtpPassword,
InternalSmtpPort,
InternalSmtpSecure,
SendGridConfig,
getEmailServerType,
getGlobalSMTPConfig,
@@ -31,98 +37,6 @@ import nodemailer, { Transporter } from "nodemailer";
import Path from "path";
import * as tls from "tls";
// Connection pool for email transporters
class TransporterPool {
private static pools: Map<string, Transporter> = new Map();
private static semaphore: Map<string, number> = new Map();
private static readonly MAX_CONCURRENT_CONNECTIONS = 100;
public static getTransporter(
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
if (!this.pools.has(key)) {
const transporter: Transporter = this.createTransporter(
emailServer,
options,
);
this.pools.set(key, transporter);
this.semaphore.set(key, 0);
}
return this.pools.get(key)!;
}
private static createTransporter(
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
if (!emailServer.secure) {
tlsOptions = {
rejectUnauthorized: false,
};
}
return nodemailer.createTransport({
host: emailServer.host.toString(),
port: emailServer.port.toNumber(),
secure: emailServer.secure,
tls: tlsOptions,
auth:
emailServer.username && emailServer.password
? {
user: emailServer.username,
pass: emailServer.password,
}
: undefined,
connectionTimeout: options.timeout || 60000,
pool: true, // Enable connection pooling
maxConnections: this.MAX_CONCURRENT_CONNECTIONS,
});
}
public static async acquireConnection(
emailServer: EmailServer,
): Promise<void> {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
while ((this.semaphore.get(key) || 0) >= this.MAX_CONCURRENT_CONNECTIONS) {
await new Promise<void>((resolve: () => void) => {
setTimeout(resolve, 100);
});
}
this.semaphore.set(key, (this.semaphore.get(key) || 0) + 1);
}
public static releaseConnection(emailServer: EmailServer): void {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const current: number = this.semaphore.get(key) || 0;
this.semaphore.set(key, Math.max(0, current - 1));
}
public static async cleanup(): Promise<void> {
const closePromises: Promise<void>[] = [];
for (const [, transporter] of this.pools) {
closePromises.push(
new Promise<void>((resolve: () => void) => {
transporter.close();
resolve();
}),
);
}
await Promise.all(closePromises);
this.pools.clear();
this.semaphore.clear();
}
}
export default class MailService {
public static isSMTPConfigValid(obj: JSONObject): boolean {
if (!obj["SMTP_USERNAME"]) {
@@ -196,6 +110,19 @@ export default class MailService {
};
}
public static getInternalEmailServer(): EmailServer {
return {
id: undefined,
username: InternalSmtpEmail.toString(),
password: InternalSmtpPassword,
host: InternalSmtpHost,
port: InternalSmtpPort,
fromEmail: InternalSmtpEmail,
fromName: InternalSmtpFromName,
secure: InternalSmtpSecure,
};
}
public static async getGlobalFromEmail(): Promise<Email> {
const emailServer: EmailServer | null = await this.getGlobalSmtpSettings();
@@ -278,7 +205,30 @@ export default class MailService {
timeout?: number | undefined;
},
): Transporter {
return TransporterPool.getTransporter(emailServer, options);
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
if (!emailServer.secure) {
tlsOptions = {
rejectUnauthorized: false,
};
}
const privateMailer: Transporter = nodemailer.createTransport({
host: emailServer.host.toString(),
port: emailServer.port.toNumber(),
secure: emailServer.secure,
tls: tlsOptions,
auth:
emailServer.username && emailServer.password
? {
user: emailServer.username,
pass: emailServer.password,
}
: undefined,
connectionTimeout: options.timeout || undefined,
});
return privateMailer;
}
private static async transportMail(
@@ -292,49 +242,12 @@ export default class MailService {
const mailer: Transporter = this.createMailer(options.emailServer, {
timeout: options.timeout,
});
let lastError: any;
const maxRetries: number = 3;
// Acquire connection slot to prevent overwhelming the server
await TransporterPool.acquireConnection(options.emailServer);
try {
for (let attempt: number = 1; attempt <= maxRetries; attempt++) {
try {
await mailer.sendMail({
from: `${options.emailServer.fromName.toString()} <${options.emailServer.fromEmail.toString()}>`,
to: mail.toEmail.toString(),
subject: mail.subject,
html: mail.body,
});
return; // Success, exit the function
} catch (error) {
lastError = error;
logger.error(`Email send attempt ${attempt} failed:`);
logger.error(error);
if (attempt === maxRetries) {
break; // Don't wait after the last attempt
}
// Wait before retrying with jitter to prevent thundering herd
const baseWaitTime: number = Math.pow(2, attempt - 1) * 1000;
const jitter: number = Math.random() * 1000; // Add up to 1 second of jitter
const waitTime: number = baseWaitTime + jitter;
await new Promise<void>((resolve: (value: void) => void) => {
setTimeout(resolve, waitTime);
});
}
}
// If we reach here, all retries failed
throw lastError;
} finally {
// Always release the connection slot
TransporterPool.releaseConnection(options.emailServer);
}
await mailer.sendMail({
from: `${options.emailServer.fromName.toString()} <${options.emailServer.fromEmail.toString()}>`,
to: mail.toEmail.toString(),
subject: mail.subject,
html: mail.body,
});
}
public static async send(
@@ -345,18 +258,6 @@ export default class MailService {
emailServer?: EmailServer | undefined;
userOnCallLogTimelineId?: ObjectID | undefined;
timeout?: number | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
userId?: ObjectID | undefined;
// On-call policy related fields
onCallPolicyId?: ObjectID | undefined;
onCallPolicyEscalationRuleId?: ObjectID | undefined;
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
onCallScheduleId?: ObjectID | undefined;
teamId?: ObjectID | undefined;
}
| undefined,
): Promise<void> {
@@ -371,48 +272,6 @@ export default class MailService {
if (options.emailServer?.id) {
emailLog.projectSmtpConfigId = options.emailServer?.id;
}
if (options.incidentId) {
emailLog.incidentId = options.incidentId;
}
if (options.alertId) {
emailLog.alertId = options.alertId;
}
if (options.scheduledMaintenanceId) {
emailLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}
if (options.statusPageId) {
emailLog.statusPageId = options.statusPageId;
}
if (options.statusPageAnnouncementId) {
emailLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
}
if (options.userId) {
emailLog.userId = options.userId;
}
if (options.teamId) {
emailLog.teamId = options.teamId;
}
// Set OnCall-related fields
if (options.onCallPolicyId) {
emailLog.onCallDutyPolicyId = options.onCallPolicyId;
}
if (options.onCallPolicyEscalationRuleId) {
emailLog.onCallDutyPolicyEscalationRuleId =
options.onCallPolicyEscalationRuleId;
}
if (options.onCallScheduleId) {
emailLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
}
}
// default vars.
@@ -575,6 +434,17 @@ export default class MailService {
options.emailServer = globalEmailServer;
}
if (
emailServerType === EmailServerType.Internal &&
(!options || !options.emailServer)
) {
if (!options) {
options = {};
}
options.emailServer = this.getInternalEmailServer();
}
if (options && options.emailServer && emailLog) {
emailLog.fromEmail = options.emailServer.fromEmail;
}
@@ -648,8 +518,4 @@ export default class MailService {
throw err;
}
}
public static async cleanup(): Promise<void> {
await TransporterPool.cleanup();
}
}

View File

@@ -1,44 +0,0 @@
import PushNotificationRequest from "Common/Types/PushNotification/PushNotificationRequest";
import ObjectID from "Common/Types/ObjectID";
import PushNotificationServiceCommon from "Common/Server/Services/PushNotificationService";
export default class PushNotificationService {
public static async send(
request: PushNotificationRequest,
options: {
projectId?: ObjectID | undefined;
isSensitive?: boolean;
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
userId?: ObjectID | undefined;
onCallPolicyId?: ObjectID | undefined;
onCallPolicyEscalationRuleId?: ObjectID | undefined;
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
onCallScheduleId?: ObjectID | undefined;
teamId?: ObjectID | undefined;
} = {},
): Promise<void> {
// Delegate to Common service which now handles logging and timeline updates
await PushNotificationServiceCommon.sendPushNotification(request, {
projectId: options.projectId,
isSensitive: Boolean(options.isSensitive),
userOnCallLogTimelineId: options.userOnCallLogTimelineId,
incidentId: options.incidentId,
alertId: options.alertId,
scheduledMaintenanceId: options.scheduledMaintenanceId,
statusPageId: options.statusPageId,
statusPageAnnouncementId: options.statusPageAnnouncementId,
userId: options.userId,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
teamId: options.teamId,
});
}
}

View File

@@ -31,18 +31,6 @@ export default class SmsService {
customTwilioConfig?: TwilioConfig | undefined;
isSensitive?: boolean; // if true, message will not be logged
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
userId?: ObjectID | undefined;
// On-call policy related fields
onCallPolicyId?: ObjectID | undefined;
onCallPolicyEscalationRuleId?: ObjectID | undefined;
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
onCallScheduleId?: ObjectID | undefined;
teamId?: ObjectID | undefined;
},
): Promise<void> {
let smsError: Error | null = null;
@@ -83,48 +71,6 @@ export default class SmsService {
smsLog.projectId = options.projectId;
}
if (options.incidentId) {
smsLog.incidentId = options.incidentId;
}
if (options.alertId) {
smsLog.alertId = options.alertId;
}
if (options.scheduledMaintenanceId) {
smsLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}
if (options.statusPageId) {
smsLog.statusPageId = options.statusPageId;
}
if (options.statusPageAnnouncementId) {
smsLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
}
if (options.userId) {
smsLog.userId = options.userId;
}
if (options.teamId) {
smsLog.teamId = options.teamId;
}
// Set OnCall-related fields
if (options.onCallPolicyId) {
smsLog.onCallDutyPolicyId = options.onCallPolicyId;
}
if (options.onCallPolicyEscalationRuleId) {
smsLog.onCallDutyPolicyEscalationRuleId =
options.onCallPolicyEscalationRuleId;
}
if (options.onCallScheduleId) {
smsLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
}
const twilioConfig: TwilioConfig | null =
options.customTwilioConfig || (await getTwilioConfig());

View File

@@ -1,28 +0,0 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=("Your OneUptime Project Subscription is overdue") }}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Project Name:" text=projectName }}
{{> DetailBoxField title="Project ID:" text=projectId }}
{{> DetailBoxField title="Subscription Status:" text="Overdue" }}
{{> DetailBoxField title="Details:" text="Some of your invoices are unpaid. Please pay the invoices to avoid deactivation of this project." }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can pay the outstanding invoices by clicking on the link below - "}}
{{> ButtonBlock buttonUrl=dashboardLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=dashboardLink}}
{{> InfoBlock info="You have been sent this email because you are the owner of this project."}}
{{> Footer this }}
{{> End this}}

View File

@@ -10,6 +10,8 @@
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="State: " text=incidentState }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}

View File

@@ -15,6 +15,8 @@
{{#if resourcesAffected}}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{/if}}
{{> DetailBoxField title="Event Description: " text="" }}
{{> DetailBoxField title="" text=eventDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}

View File

@@ -12,7 +12,6 @@ import Realtime from "Common/Server/Utils/Realtime";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import "ejs";
import OpenAPIUtil from "Common/Server/Utils/OpenAPI";
const APP_NAME: string = "api";
@@ -97,9 +96,6 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
// Add default routes to the app
await App.addDefaultRoutes();
// Generate OpenAPI spec (this automatically saves it to cache)
OpenAPIUtil.generateOpenAPISpec();
} catch (err) {
logger.error("App Init Failed:");
logger.error(err);

21
App/package-lock.json generated
View File

@@ -35,9 +35,8 @@
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.2",
"@bull-board/express": "^5.21.4",
"@clickhouse/client": "^1.10.1",
"@clickhouse/client": "^0.2.10",
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
@@ -65,19 +64,18 @@
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^8.3.4",
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"cookie-parser": "^1.4.7",
"Common": "file:../Common",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"express": "^4.19.2",
"formik": "^2.4.6",
"history": "^5.3.0",
"ioredis": "^5.3.2",
@@ -85,6 +83,7 @@
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.0",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"marked": "^12.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
@@ -92,7 +91,6 @@
"nodemailer": "^6.9.10",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
@@ -115,7 +113,6 @@
"redis-semaphore": "^5.5.1",
"reflect-metadata": "^0.2.2",
"remark-gfm": "^3.0.1",
"slackify-markdown": "^4.4.0",
"slugify": "^1.6.5",
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.5",
@@ -125,11 +122,9 @@
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"universal-cookie": "^4.0.4",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"uuid": "^8.3.2"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -143,6 +138,7 @@
"@types/jest": "^28.1.4",
"@types/json2csv": "^5.0.3",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.202",
"@types/node": "^17.0.45",
"@types/node-cron": "^3.0.7",
"@types/nodemailer": "^6.4.7",
@@ -156,7 +152,6 @@
"jest-environment-jsdom": "^29.7.0",
"jest-mock-extended": "^3.0.5",
"react-test-renderer": "^18.2.0",
"sass": "^1.89.2",
"ts-jest": "^28.0.5"
}
},

View File

@@ -140,6 +140,13 @@ export default class AnalyticsBaseModel extends CommonModel {
this.crudApiPath = data.crudApiPath;
this.enableRealtimeEventsOn = data.enableRealtimeEventsOn;
this.partitionKey = data.partitionKey;
// initialize Arrays.
for (const column of this.tableColumns) {
if (column.type === TableColumnType.NestedModel) {
this.setColumnValue(column.key, []);
}
}
}
private _enableWorkflowOn: EnableWorkflowOn | undefined;

View File

@@ -76,7 +76,7 @@ export default class CommonModel {
if (column.type === TableColumnType.JSON && typeof value === "string") {
try {
value = JSONFunctions.parse(value);
} catch {
} catch (e) {
value = {};
}
}
@@ -91,7 +91,7 @@ export default class CommonModel {
if (!Array.isArray(value)) {
throw new BadDataException("Not an array");
}
} catch {
} catch (e) {
value = [];
}
}
@@ -207,6 +207,19 @@ export default class CommonModel {
return;
}
if (recordValue instanceof Array) {
if (recordValue.length > 0 && column.nestedModelType) {
json[column.key] = CommonModel.toJSONArray(
recordValue as Array<CommonModel>,
column.nestedModelType,
);
} else {
json[column.key] = recordValue;
}
return;
}
json[column.key] = recordValue;
});

View File

@@ -0,0 +1,8 @@
import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
import CommonModel from "./CommonModel";
export default class NestedModel extends CommonModel {
public constructor(data: { tableColumns: Array<AnalyticsTableColumn> }) {
super(data);
}
}

View File

@@ -12,8 +12,8 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
super({
tableName: "ExceptionItem",
tableEngine: AnalyticsTableEngine.MergeTree,
singularName: "Exception Instance",
pluralName: "Exception Instances",
singularName: "Exception",
pluralName: "Exceptions",
enableRealtimeEventsOn: {
create: true,
},

View File

@@ -0,0 +1,59 @@
import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
import NestedModel from "../AnalyticsBaseModel/NestedModel";
export default class KeyValueNestedModel extends NestedModel {
public constructor() {
super({
tableColumns: [
new AnalyticsTableColumn({
key: "key",
title: "Key",
description: "Key of the attribute",
required: true,
type: TableColumnType.Text,
}),
new AnalyticsTableColumn({
key: "stringValue",
title: "String Value",
description: "Key of the attribute",
required: false,
type: TableColumnType.Text,
}),
new AnalyticsTableColumn({
key: "numberValue",
title: "Number Value",
description: "Value of the attribute",
required: false,
type: TableColumnType.Number,
}),
],
});
}
public get key(): string | undefined {
return this.getColumnValue("key");
}
public set key(v: string | undefined) {
this.setColumnValue("key", v);
}
public get stringValue(): string | undefined {
return this.getColumnValue("stringValue");
}
public set stringValue(v: string | undefined) {
this.setColumnValue("stringValue", v);
}
public get numberValue(): number | undefined {
return this.getColumnValue("numberValue");
}
public set numberValue(v: number | undefined) {
this.setColumnValue("numberValue", v);
}
}

View File

@@ -104,7 +104,6 @@ export default class AcmeCertificate extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -80,7 +80,6 @@ export default class AcmeChallenge extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -217,7 +217,7 @@ export default class Alert extends BaseModel {
type: TableColumnType.Markdown,
title: "Description",
description:
"Short description of this alert. This will be visible on the status page. This is in markdown.",
"Short description of this alert. This is in markdown and will be visible on the status page.",
})
@Column({
nullable: true,
@@ -299,7 +299,6 @@ export default class Alert extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -571,13 +570,11 @@ export default class Alert extends BaseModel {
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
isDefaultValueColumn: true,
title: "Current Alert State ID",
description: "Current Alert State ID",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
@@ -761,7 +758,12 @@ export default class Alert extends BaseModel {
public customFields?: JSONObject = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlert,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -773,13 +775,10 @@ export default class Alert extends BaseModel {
@Index()
@TableColumn({
type: TableColumnType.Boolean,
computed: true,
hideColumnInDocumentation: true,
required: true,
isDefaultValueColumn: true,
title: "Are Owners Notified Of Alert Creation?",
description: "Are owners notified of when this alert is created?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
@@ -937,7 +936,6 @@ export default class Alert extends BaseModel {
title: "Is created automatically?",
description:
"Is this alert created by OneUptime Probe or Workers automatically (and not created manually by a user)?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
@@ -1030,7 +1028,6 @@ export default class Alert extends BaseModel {
isDefaultValueColumn: false,
required: false,
type: TableColumnType.Number,
computed: true,
title: "Alert Number",
description: "Alert Number",
})

View File

@@ -218,7 +218,7 @@ export default class AlertCustomField extends BaseModel {
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public customFieldType?: CustomFieldType = undefined;
public type?: CustomFieldType = undefined;
@ColumnAccessControl({
create: [
@@ -297,7 +297,6 @@ export default class AlertCustomField extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -282,7 +282,6 @@ export default class AlertFeed extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -271,7 +271,6 @@ export default class AlertInternalNote extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -341,7 +340,12 @@ export default class AlertInternalNote extends BaseModel {
public note?: string = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlertInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -353,13 +357,10 @@ export default class AlertInternalNote extends BaseModel {
@Index()
@TableColumn({
type: TableColumnType.Boolean,
computed: true,
hideColumnInDocumentation: true,
required: true,
isDefaultValueColumn: true,
title: "Are Owners Notified",
description: "Are owners notified of this resource ownership?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,

View File

@@ -314,7 +314,6 @@ export default class AlertNoteTemplate extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -64,7 +64,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@Entity({
name: "AlertOwnerTeam",
})
@Index(["alertId", "teamId", "projectId"])
export default class AlertOwnerTeam extends BaseModel {
@ColumnAccessControl({
create: [
@@ -344,7 +343,6 @@ export default class AlertOwnerTeam extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -408,7 +406,6 @@ export default class AlertOwnerTeam extends BaseModel {
isDefaultValueColumn: true,
title: "Are Owners Notified",
description: "Are owners notified of this resource ownership?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,

View File

@@ -63,7 +63,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@Entity({
name: "AlertOwnerUser",
})
@Index(["alertId", "userId", "projectId"])
export default class AlertOwnerUser extends BaseModel {
@ColumnAccessControl({
create: [
@@ -343,7 +342,6 @@ export default class AlertOwnerUser extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -407,7 +405,6 @@ export default class AlertOwnerUser extends BaseModel {
isDefaultValueColumn: true,
title: "Are Owners Notified",
description: "Are owners notified of this resource ownership?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,

View File

@@ -76,7 +76,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@Entity({
name: "AlertSeverity",
})
@Index(["projectId", "order"])
export default class AlertSeverity extends BaseModel {
@ColumnAccessControl({
create: [
@@ -193,7 +192,6 @@ export default class AlertSeverity extends BaseModel {
required: true,
unique: true,
type: TableColumnType.Slug,
computed: true,
title: "Slug",
description: "Friendly globally unique name for your object",
})
@@ -316,7 +314,6 @@ export default class AlertSeverity extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -76,10 +76,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@Entity({
name: "AlertState",
})
@Index(["projectId", "isCreatedState"])
@Index(["projectId", "isResolvedState"])
@Index(["projectId", "isAcknowledgedState"])
@Index(["projectId", "order"])
export default class AlertState extends BaseModel {
@ColumnAccessControl({
create: [
@@ -294,7 +290,6 @@ export default class AlertState extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -60,7 +60,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@Entity({
name: "AlertStateTimeline",
})
@Index(["alertId", "startsAt"])
@TableMetadata({
tableName: "AlertStateTimeline",
singularName: "Alert State Timeline",
@@ -275,7 +274,6 @@ export default class AlertStateTimeline extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -389,7 +387,12 @@ export default class AlertStateTimeline extends BaseModel {
public alertStateId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlertStateTimeline,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -401,13 +404,10 @@ export default class AlertStateTimeline extends BaseModel {
@Index()
@TableColumn({
type: TableColumnType.Boolean,
computed: true,
hideColumnInDocumentation: true,
required: true,
isDefaultValueColumn: true,
title: "Are Owners Notified",
description: "Are owners notified of state change?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,

View File

@@ -201,7 +201,6 @@ export default class ApiKey extends BaseModel {
required: true,
unique: true,
type: TableColumnType.Slug,
computed: true,
title: "Slug",
description: "Friendly globally unique name for your object",
})
@@ -282,7 +281,6 @@ export default class ApiKey extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -349,7 +347,11 @@ export default class ApiKey extends BaseModel {
public expiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadProjectApiKey,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -366,7 +368,6 @@ export default class ApiKey extends BaseModel {
type: TableColumnType.ObjectID,
isDefaultValueColumn: false,
title: "API Key",
computed: true,
description: "Secret API Key",
})
@Column({

View File

@@ -291,7 +291,6 @@ export default class APIKeyPermission extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -426,11 +425,7 @@ export default class APIKeyPermission extends BaseModel {
Permission.EditProjectApiKey,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@TableColumn({ isDefaultValueColumn: true, type: TableColumnType.Boolean })
@Column({
type: ColumnType.Boolean,
default: false,

View File

@@ -172,7 +172,6 @@ export default class BillingInvoice extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -174,7 +174,6 @@ export default class BillingPaymentMethod extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -235,7 +234,7 @@ export default class BillingPaymentMethod extends BaseModel {
nullable: false,
unique: false,
})
public paymentMethodType?: string = undefined;
public type?: string = undefined;
@ColumnAccessControl({
create: [],

View File

@@ -1,14 +1,5 @@
import Project from "./Project";
import Incident from "./Incident";
import Alert from "./Alert";
import ScheduledMaintenance from "./ScheduledMaintenance";
import StatusPage from "./StatusPage";
import StatusPageAnnouncement from "./StatusPageAnnouncement";
import User from "./User";
import OnCallDutyPolicy from "./OnCallDutyPolicy";
import OnCallDutyPolicyEscalationRule from "./OnCallDutyPolicyEscalationRule";
import OnCallDutyPolicySchedule from "./OnCallDutyPolicySchedule";
import Team from "./Team";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import CallStatus from "../../Types/Call/CallStatus";
@@ -256,7 +247,6 @@ export default class CallLog extends BaseModel {
description: "Call Cost in USD Cents",
canReadOnRelationQuery: false,
isDefaultValueColumn: true,
defaultValue: 0,
})
@Column({
nullable: false,
@@ -265,575 +255,6 @@ export default class CallLog extends BaseModel {
})
public callCostInUSDCents?: number = undefined;
// Relations to resources that triggered this Call (nullable)
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "incidentId",
type: TableColumnType.Entity,
modelType: Incident,
title: "Incident",
description: "Incident associated with this Call (if any)",
})
@ManyToOne(
() => {
return Incident;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "incidentId" })
public incident?: Incident = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Incident ID",
description: "ID of Incident associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public incidentId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User who initiated this Call (if any)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User ID",
description: "ID of User who initiated this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "alertId",
type: TableColumnType.Entity,
modelType: Alert,
title: "Alert",
description: "Alert associated with this Call (if any)",
})
@ManyToOne(
() => {
return Alert;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "alertId" })
public alert?: Alert = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Alert ID",
description: "ID of Alert associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public alertId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "scheduledMaintenanceId",
type: TableColumnType.Entity,
modelType: ScheduledMaintenance,
title: "Scheduled Maintenance",
description: "Scheduled Maintenance associated with this Call (if any)",
})
@ManyToOne(
() => {
return ScheduledMaintenance;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "scheduledMaintenanceId" })
public scheduledMaintenance?: ScheduledMaintenance = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Scheduled Maintenance ID",
description:
"ID of Scheduled Maintenance associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public scheduledMaintenanceId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status Page associated with this Call (if any)",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page ID",
description: "ID of Status Page associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageAnnouncementId",
type: TableColumnType.Entity,
modelType: StatusPageAnnouncement,
title: "Status Page Announcement",
description: "Status Page Announcement associated with this Call (if any)",
})
@ManyToOne(
() => {
return StatusPageAnnouncement;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageAnnouncementId" })
public statusPageAnnouncement?: StatusPageAnnouncement = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page Announcement ID",
description:
"ID of Status Page Announcement associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageAnnouncementId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicy,
title: "On-Call Duty Policy",
description: "On-Call Duty Policy associated with this Call (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicy;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyId" })
public onCallDutyPolicy?: OnCallDutyPolicy = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy ID",
description: "ID of On-Call Duty Policy associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyEscalationRuleId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicyEscalationRule,
title: "On-Call Duty Policy Escalation Rule",
description:
"On-Call Duty Policy Escalation Rule associated with this Call (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicyEscalationRule;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyEscalationRuleId" })
public onCallDutyPolicyEscalationRule?: OnCallDutyPolicyEscalationRule =
undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy Escalation Rule ID",
description:
"ID of On-Call Duty Policy Escalation Rule associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyEscalationRuleId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyScheduleId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicySchedule,
title: "On-Call Duty Policy Schedule",
description:
"On-Call Duty Policy Schedule associated with this Call (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicySchedule;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyScheduleId" })
public onCallDutyPolicySchedule?: OnCallDutyPolicySchedule = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy Schedule ID",
description:
"ID of On-Call Duty Policy Schedule associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyScheduleId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "teamId",
type: TableColumnType.Entity,
modelType: Team,
title: "Team",
description: "Team associated with this Call (if any)",
})
@ManyToOne(
() => {
return Team;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "teamId" })
public team?: Team = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadCallLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Team ID",
description: "ID of Team associated with this Call (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public teamId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
@@ -843,7 +264,6 @@ export default class CallLog extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -289,7 +289,6 @@ export default class CopilotAction extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -729,7 +728,6 @@ export default class CopilotAction extends BaseModel {
type: TableColumnType.Boolean,
title: "Is Priority",
description: "Is Priority",
defaultValue: false,
})
@Column({
nullable: false,

View File

@@ -250,7 +250,6 @@ export default class CopilotActionTypePriority extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})

View File

@@ -208,7 +208,6 @@ export default class CopilotCodeRepository extends BaseModel {
required: true,
unique: true,
type: TableColumnType.Slug,
computed: true,
title: "Slug",
description: "Friendly globally unique name for your object",
})
@@ -335,7 +334,6 @@ export default class CopilotCodeRepository extends BaseModel {
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@@ -427,7 +425,12 @@ export default class CopilotCodeRepository extends BaseModel {
public labels?: Array<Label> = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateCopilotCodeRepository,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -446,7 +449,6 @@ export default class CopilotCodeRepository extends BaseModel {
@TableColumn({
type: TableColumnType.ObjectID,
isDefaultValueColumn: false,
computed: true,
title: "Secret Token",
description:
"Secret Token for this code repository. This is used to connect this code repository to OneUptime.",

Some files were not shown because too many files have changed in this diff Show More