Compare commits

...

683 Commits

Author SHA1 Message Date
Nawaz Dhandala
3ff0861d68 Merge branch 'master' into files-notes 2025-11-12 13:04:19 +00:00
Nawaz Dhandala
fcf919c70b Refactor session handling and cookie management in StatusPage authentication 2025-11-12 12:52:19 +00:00
Nawaz Dhandala
f0f3d32d31 Merge remote-tracking branch 'origin/snyk-upgrade-b70b2734abb0e16d5d110c8cd2735c35' 2025-11-12 11:49:20 +00:00
Nawaz Dhandala
444e8f17b6 Implement feature X to enhance user experience and fix bug Y in module Z 2025-11-12 11:46:44 +00:00
Nawaz Dhandala
3aabf44b4e Merge branch 'snyk-upgrade-f017994c6dac770941ee664640830ac7' 2025-11-12 11:45:47 +00:00
Simon Larsen
c11fcc3c8e Merge pull request #2106 from OneUptime/user-refresh-token
User refresh token
2025-11-12 11:42:29 +00:00
Simon Larsen
52519c9af8 Merge pull request #2104 from OneUptime/snyk-upgrade-d659682fbd62c498810b328f5aaca524
[Snyk] Upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0
2025-11-12 11:42:08 +00:00
Simon Larsen
2483cf9499 Merge pull request #2102 from OneUptime/snyk-upgrade-289d4467acd89e4854c2a2dc61916341
[Snyk] Upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0
2025-11-12 11:41:44 +00:00
Simon Larsen
634e21b13c Merge pull request #2101 from OneUptime/snyk-upgrade-8678075385220dcc2c31b1b4a3900956
[Snyk] Upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0
2025-11-12 11:41:35 +00:00
Nawaz Dhandala
aad933b9eb feat(Authentication, Session Management): implement finalizeStatusPageLogin and refresh-token endpoints for enhanced session handling 2025-11-12 11:40:52 +00:00
Nawaz Dhandala
9356f2964e feat(Authentication): integrate UserSession model and enhance finalizeUserLogin type definition
feat(Express): define HeaderValue type and improve type annotations for headerValueToString and extractDeviceInfo functions
2025-11-11 21:41:20 +00:00
Nawaz Dhandala
aae70ead3b refactor: streamline code formatting and improve readability across multiple files 2025-11-11 21:36:12 +00:00
Nawaz Dhandala
8a482dce10 feat(UserSession): enhance session management with comprehensive session handling methods and metadata 2025-11-11 21:34:11 +00:00
Nawaz Dhandala
9fdf46889c feat(Text): add truncate method for string length limitation 2025-11-11 21:32:57 +00:00
Nawaz Dhandala
40ca9dc04c feat(Authentication, SSO): enhance session management with user session creation and refresh token handling 2025-11-11 21:22:21 +00:00
Nawaz Dhandala
74937f2208 feat(DeviceInfo): add extractDeviceInfo function and RequestDeviceInfo type for enhanced device data retrieval 2025-11-11 21:20:07 +00:00
Nawaz Dhandala
c02ab56477 feat(Cookie): enhance cookie management with refresh token support and default access token expiry 2025-11-11 21:11:34 +00:00
Nawaz Dhandala
3f99b9680f feat(Migration): add UserSession and StatusPagePrivateUserSession migrations with constraints and indexes 2025-11-11 19:51:54 +00:00
Nawaz Dhandala
b08c39037d feat(Index): add StatusPagePrivateUserSessionService and UserSessionService to services 2025-11-11 19:49:32 +00:00
Nawaz Dhandala
f7cc3c00da feat(Migration): add migration for StatusPagePrivateUserSession and UserSession tables 2025-11-11 19:48:42 +00:00
Nawaz Dhandala
ac4286935a refactor(StatusPagePrivateUserSession): remove unnecessary blank line for cleaner code
refactor(UserSession): format description for additional info column for improved readability
2025-11-11 19:47:08 +00:00
Nawaz Dhandala
90a0b2e4a8 refactor(StatusPagePrivateUserSession): simplify access control by removing specific permissions 2025-11-11 19:46:19 +00:00
Nawaz Dhandala
9b22c48d27 feat(UserSession): add UserSession model for managing active user sessions and security tokens 2025-11-11 19:41:41 +00:00
Nawaz Dhandala
9c9dad5da0 feat(UserSettings): add user settings page and integrate into side menu 2025-11-11 19:18:20 +00:00
Nawaz Dhandala
e986f74025 fix(TeamMemberService): skip one-member guard when SCIM manages project membership 2025-11-11 19:07:11 +00:00
snyk-bot
deb2e81b21 fix: upgrade @opentelemetry/exporter-trace-otlp-proto from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-trace-otlp-proto from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-trace-otlp-proto

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:46:09 +00:00
snyk-bot
0f8b322892 fix: upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-trace-otlp-http from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-trace-otlp-http

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:46:03 +00:00
snyk-bot
23c7de3ecd fix: upgrade @opentelemetry/exporter-metrics-otlp-proto from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-metrics-otlp-proto from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-metrics-otlp-proto

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:57 +00:00
snyk-bot
ad144a6240 fix: upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/exporter-logs-otlp-http from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/exporter-logs-otlp-http

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:52 +00:00
snyk-bot
debfef0388 fix: upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0
Snyk has created this PR to upgrade @opentelemetry/sdk-logs from 0.52.1 to 0.207.0.

See this package in npm:
@opentelemetry/sdk-logs

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-11 17:45:46 +00:00
Nawaz Dhandala
bb85c9f8c8 refactor(BaseModelTable): enhance filter function for dropdown labels to improve type safety 2025-11-11 17:23:14 +00:00
Nawaz Dhandala
25ab1cdbf9 refactor(BaseModelTable): improve code formatting and simplify access control value handling 2025-11-11 17:21:51 +00:00
Nawaz Dhandala
44b8a9ddc9 feat(BaseModelTable): integrate access control column handling and update dropdown labels with color support 2025-11-11 17:16:52 +00:00
Nawaz Dhandala
c388ff9550 fix(IncidentCreate): remove unnecessary whitespace in color mapping function 2025-11-11 15:59:04 +00:00
Nawaz Dhandala
321d1680e6 feat(IncidentCreate): enhance incident state options with color attribute 2025-11-11 15:58:13 +00:00
Nawaz Dhandala
6c0e9f0fed refactor: remove unnecessary whitespace in model classes and improve code formatting 2025-11-11 15:49:20 +00:00
Nawaz Dhandala
99349ecb30 fix: restore @ColorField decorator in multiple model classes for color handling 2025-11-11 15:47:57 +00:00
Nawaz Dhandala
258bbbd9cf feat(BaseModelTable): enhance dropdown options with color handling based on the first color column 2025-11-11 15:37:56 +00:00
Nawaz Dhandala
1094a07fc6 fix(DropdownUtil): ensure color variable is explicitly typed as Color | null
refactor(ColorField): add return type annotation for ColorField function
2025-11-11 14:30:33 +00:00
Nawaz Dhandala
14a5671645 feat(Color, Dropdown, ModelForm): enhance color handling in Color class and Dropdown options 2025-11-11 14:05:30 +00:00
Nawaz Dhandala
5a41c66953 refactor(DatabaseBaseModel, ColorField): improve formatting and readability of type definitions and method implementations 2025-11-11 13:26:02 +00:00
Nawaz Dhandala
af605fce4c feat(DatabaseBaseModel): add getFirstColorColumn method to retrieve the first color field column 2025-11-11 13:23:55 +00:00
Nawaz Dhandala
f8ef6c69fe feat(ColorField): add ColorField decorator and related utility functions for color field management in database models 2025-11-11 13:20:42 +00:00
Simon Larsen
e1848f44f7 Merge pull request #2100 from OneUptime/dropdown-lbl
Dropdown lbl
2025-11-11 12:49:54 +00:00
Nawaz Dhandala
825bd39dda feat(Dropdown): enhance type definitions for styling functions and improve label rendering 2025-11-11 12:49:25 +00:00
Nawaz Dhandala
b99905dfe8 fix(Telemetry): add npm install step for Common directory in workflow 2025-11-11 12:45:06 +00:00
Nawaz Dhandala
a4bf40a2c1 fix(Label): correct indentation for icon property in LabelElement 2025-11-11 12:44:05 +00:00
Nawaz Dhandala
711998b048 feat(Icon): add strokeWidth to EmptyCircle icon rendering 2025-11-11 12:42:36 +00:00
Nawaz Dhandala
132e044c07 feat(Icon): add EmptyCircle icon and update Label to use it 2025-11-11 12:16:01 +00:00
Nawaz Dhandala
8ecc307451 feat(Dropdown): implement label rendering and color resolution for selected labels 2025-11-11 12:02:49 +00:00
Nawaz Dhandala
c85c29989f feat(Dropdown): update styling for focused and selected states, enhance multi-value appearance 2025-11-10 23:29:16 +00:00
Nawaz Dhandala
95726e0f21 feat(Dropdown): enhance dropdown options with label support and improve styling 2025-11-10 23:13:10 +00:00
Nawaz Dhandala
adc15992e9 refactor(Label): simplify import statements for Pill component 2025-11-10 22:49:36 +00:00
Nawaz Dhandala
58d83a2a80 feat(Pill): add thickness prop to Icon component in Pill 2025-11-10 22:48:57 +00:00
Nawaz Dhandala
5461cd4502 feat(Pill): add icon support to Pill component and update tests 2025-11-10 22:44:07 +00:00
Nawaz Dhandala
478465a65b refactor: update label imports and restructure label components for consistency 2025-11-10 22:34:33 +00:00
Nawaz Dhandala
bef57b784d Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2025-11-10 16:38:32 +00:00
Nawaz Dhandala
601eed0e70 refactor(Page): enhance layout and spacing for title and labels section
refactor(ModelPage): improve formatting of labelsColumn assignment
refactor(DatabaseBaseModel): remove unnecessary blank line
2025-11-10 16:31:29 +00:00
Nawaz Dhandala
dc1215f28b refactor(Page): adjust title font size for improved readability 2025-11-10 15:55:44 +00:00
Nawaz Dhandala
f617330484 refactor: remove labelsColumn and associated method, update ModelPage to use getAccessControlColumn 2025-11-10 15:53:44 +00:00
Simon Larsen
888d8d6fbf refactor: clean up code formatting and improve readability across multiple files 2025-11-10 14:36:44 +00:00
Simon Larsen
79a9bb2229 chore(notification): include WhatsApp in balance, auto-recharge and notification copy
Update Project model and NotificationSettings UI text to mention WhatsApp alongside SMS and Call for current balance, auto-recharge settings, and related notifications.
2025-11-10 14:15:47 +00:00
Simon Larsen
96f0b111c1 chore(forms/ui/models): normalize imports/formatting, minor type & mapping fixes
- Reformat typeorm imports in note DB models for consistency
- Normalize JSX/indentation in FilePicker and FileList
- Add missing Link import to EventItem
- Strengthen filter type annotation in FileList
- Clarify MultipleFiles -> FieldType.File mapping in FormFieldSchemaTypeUtil
- Minor formatting cleanups in FormField and dashboard note view files
2025-11-10 14:01:37 +00:00
Simon Larsen
044ec492da feat(forms): add MultipleFiles field type and multi-file handling
- Add MultipleFiles to FormFieldSchemaType enum.
- Map MultipleFiles to FieldType.File in FormFieldSchemaTypeUtil.
- Enhance FilePicker to append new uploads for multi-picker, deduplicate by id, and return updated file list.
- Update FormField to pass isMultiFilePicker, normalize/strip FileModel fields, and set array or single value based on field type.
- Replace file + isMultiFilePicker usage in various note/public/internal pages with MultipleFiles.
2025-11-10 13:48:32 +00:00
Simon Larsen
6d4462c969 feat(notes): add file attachments support to notes (models, services, API, UI, status page)
- Add attachments ManyToMany relation (File) + JoinTable to Alert/Incident/ScheduledMaintenance
  internal/public note models and table metadata.
- Update note services to accept attachments: map File | ObjectID to File instances on create.
- Include attachments fields in StatusPageAPI selections for notes.
- Add new FileList UI component to render attachment links.
- Integrate file picker/display in Dashboard note pages (alerts, incidents, scheduled maintenance):
  add form field, selectMoreFields, and render attachments alongside markdown.
- Include attachments on Status Page timeline items so public notes display files.
2025-11-10 13:37:15 +00:00
Simon Larsen
0578f9f1be Merge pull request #2092 from OneUptime/master
Release
2025-11-10 09:35:46 +00:00
Simon Larsen
ebbddbd797 Merge pull request #2091 from OneUptime/snyk-fix-633d6e2055ebd39def346bdc57989683
[Snyk] Security upgrade nginx from 1.29.2-alpine to 1.29.3-alpine
2025-11-10 09:35:11 +00:00
Simon Larsen
6cdaaf1489 Merge pull request #2090 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-10 09:35:02 +00:00
snyk-bot
93e62befbb fix: Nginx/Dockerfile.tpl to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE322-PCRE2-13637025
2025-11-08 10:56:45 +00:00
simlarsen
bccd2f484b chore: npm audit fix 2025-11-08 01:40:56 +00:00
Nawaz Dhandala
c01fc9ee64 chore(migration): drop jwtRefreshToken column from User and StatusPagePrivateUser tables
- remove jwtRefreshToken property from User model
- add migration to drop column from User and StatusPagePrivateUser and restore it on down
- register new migration in SchemaMigrations index
2025-11-07 22:37:25 +00:00
Simon Larsen
be9dcbd01e Merge pull request #2089 from OneUptime/otel-ingest-rename
Otel ingest rename
2025-11-07 22:22:27 +00:00
Nawaz Dhandala
48f85df04c fix(header): use w-64 instead of max-w-64 for ProjectPicker container to enforce fixed width 2025-11-07 22:01:32 +00:00
Nawaz Dhandala
fc08578ff2 chore(telemetry): remove openTelemetryCollectorHost and OTEL collector nginx/ingress config 2025-11-07 21:49:42 +00:00
Nawaz Dhandala
ea9a245b82 chore(nginx): rename open-telemetry-ingest upstream to telemetry and update proxy_pass targets (/telemetry, fluentd and syslog endpoints) 2025-11-07 21:42:26 +00:00
Nawaz Dhandala
f49b1995df feat(telemetry): add new Telemetry service (OTel, Syslog, Fluent, Metrics, Traces) and unified ingestion pipeline
- Add Telemetry service entrypoint
  - Telemetry/Index.ts: app bootstrap, routes mounting, infrastructure init and Telemetry SDK init.

- Unified queue + worker
  - Telemetry/Jobs/TelemetryIngest/ProcessTelemetry.ts: single worker that dispatches queued jobs to specific processors (logs, traces, metrics, syslog, fluent logs).
  - Telemetry/Services/Queue/TelemetryQueueService.ts: central queue API and job payload types.
  - Per-type Queue wrappers (LogsQueueService, MetricsQueueService, TracesQueueService, FluentLogsQueueService, SyslogQueueService).

- OpenTelemetry ingestion middleware and proto support
  - Telemetry/Middleware/OtelRequestMiddleware.ts: detect OTLP endpoint (logs/traces/metrics), decode protobuf bodies using protobufjs and set product type.
  - Telemetry/ProtoFiles/OTel/v1/*.proto: include common.proto, logs.proto, metrics.proto, resource.proto, traces.proto for OTLP v1 messages.

- Ingest services
  - Telemetry/Services/OtelLogsIngestService.ts: parse incoming OTLP logs, map attributes, convert timestamps, batch insert logs.
  - Telemetry/Services/OtelTracesIngestService.ts: parse OTLP traces, build span rows, extract exceptions, batch insert spans and exceptions, save telemetry exception summary.
  - Telemetry/Services/OtelMetricsIngestService.ts: parse OTLP metrics, normalize datapoints, batch insert metrics and index metric name -> service map.
  - Telemetry/Services/SyslogIngestService.ts: syslog ingestion endpoints, parser integration, map syslog fields to attributes and logs.
  - Telemetry/Services/FluentLogsIngestService.ts: ingest Fluentd style logs, normalize entries and insert into log backend.
  - Telemetry/Services/OtelIngestBaseService.ts: helpers to resolve service name from attributes/headers.

- Syslog parser and utilities
  - Telemetry/Utils/SyslogParser.ts: robust RFC5424 and RFC3164 parser, structured data extraction and sanitization.
  - Telemetry/Tests/Utils/SyslogParser.test.ts: unit tests for parser behavior.

- Telemetry exception utilities
  - Telemetry/Utils/Exception.ts: generate exception fingerprint and upsert telemetry exception status (saveOrUpdateTelemetryException).

- Queue & job integration
  - New integration with Common/Server/Infrastructure/Queue and QueueWorker, job id generation and telemetry job types.
  - Telemetry services add ingestion jobs instead of processing synchronously.

- Config, build and dev tooling
  - Add Telemetry/package.json, package-lock.json, tsconfig.json, nodemon.json, jest config.
  - New script configs and dependencies (protobufjs, ts-node, jest, nodemon, etc).

- Docker / environment updates
  - docker-compose.base.yml, docker-compose.dev.yml, docker-compose.yml: rename service from open-telemetry-ingest -> telemetry and wire TELEMETRY_* envs.
  - config.example.env: rename and consolidate environment variables (OPEN_TELEMETRY_* -> TELEMETRY_*, update hostnames and ports).
  - Tests/Scripts/status-check.sh: update ready-check target to telemetry/status/ready.

- Other
  - Telemetry/Services/Queue/*: export helpers and legacy-compatible job interface shims.
  - Memory cleanup and batching safeguards across ingest services.
  - Logging and capture spans added to key code paths.

BREAKING CHANGES / MIGRATION NOTES:
- Environment variables and docker service names changed:
  - Replace OPEN_TELEMETRY_... vars with TELEMETRY_... (PORT, HOSTNAME, CONCURRENCY, DISABLE_TELEMETRY, etc).
  - docker-compose entries moved from "open-telemetry-ingest" to "telemetry" and image name changed to oneuptime/telemetry.
  - Update any deployment automation and monitoring checks referencing the old service name or endpoints.
- Consumers: OTLP endpoints and behavior remain supported, but ingestion is now queued and processed asynchronously.

Testing / Running:
- Install deps in Telemetry/ (npm install) after syncing Common workspace.
- Run dev: npx nodemon (nodemon.json) or build & start using provided scripts.
- Run tests with jest (Telemetry test suite includes SyslogParser unit tests).

Files added/modified (high level):
- Added many files under Telemetry/: Index, Jobs, Middleware, ProtoFiles, Services, Utils, Tests, package and config artifacts.
- Modified docker-compose.* and config.example.env and status check script to use new TELEMETRY service/vars.
2025-11-07 21:36:47 +00:00
Simon Larsen
351fc4828b Merge pull request #2088 from OneUptime/merge-fluentd
Merge fluentd
2025-11-07 20:40:14 +00:00
Nawaz Dhandala
04cb7da6d6 chore(fluentd): update production endpoint and x-oneuptime token in Fluentd config (Fluentd/fluent.conf) 2025-11-07 20:38:23 +00:00
Nawaz Dhandala
c6e2f41351 chore(docker-compose): add fluentd service to docker-compose.base.yml 2025-11-07 20:27:50 +00:00
Simon Larsen
e524dfda6d chore(fluent-ingest): normalize formatting in FluentLogsIngestService and Index.ts 2025-11-07 20:22:51 +00:00
Simon Larsen
30b6353714 chore(fluent-ingest): route fluent log ingest through TelemetryQueueService, add FluentLogsIngestService and remove legacy ProcessFluentLogs job 2025-11-07 20:20:25 +00:00
Simon Larsen
29be9b399f chore(fluent-ingest): route fluent log ingest through TelemetryQueueService and remove legacy queue endpoints
- Replace FluentLogsQueueService import/use with TelemetryQueueService.addFluentLogIngestJob in Fluent API.
- Remove ClusterKeyAuthorization-protected queue admin endpoints (stats, size, failed) from Fluent API.
- Remove ProcessFluentLogs job import from service initialization.
2025-11-07 20:02:22 +00:00
Simon Larsen
36cdeec916 chore(queue): remove FluentLogs from QueueName enum 2025-11-07 19:46:29 +00:00
Simon Larsen
a80b7ba88c chore(fluent-ingest): migrate fluent log ingest into open-telemetry-ingest and remove legacy fluent-ingest service
- Move Fluent/Fluent Bit logs ingestion into open-telemetry-ingest:
  - Add OpenTelemetryIngest/API/Fluent.ts (routes for /fluentd and queue endpoints)
  - Add Queue service, job worker and processor:
    - OpenTelemetryIngest/Services/Queue/FluentLogsQueueService.ts
    - OpenTelemetryIngest/Jobs/TelemetryIngest/ProcessFluentLogs.ts
  - Register Fluent API and job processing in OpenTelemetryIngest/Index.ts
  - Introduce QueueName.FluentLogs and related queue usage

- Remove legacy FluentIngest service and configuration:
  - Delete fluent-ingest docker-compose/dev/base entries and docker-compose.yml service
  - Remove fluent-ingest related helm values, KEDA scaledobject, ingress host and schema entries
  - Remove FLUENTD_HOST env/values and replace FLUENT_INGEST_HOSTNAME -> FLUENT_LOGS_HOSTNAME (pointing to open-telemetry-ingest)
  - Update config.example.env keys (FLUENT_LOGS_CONCURRENCY, DISABLE_TELEMETRY_FOR_FLUENT_LOGS)
  - Remove FluentIngestRoute and FLUENT_INGEST_URL/hostname usages from UI config/templates
  - Remove VSCode launch debug config for Fluent Ingest
  - Remove Fluent ingest E2E status check entry in Tests/Scripts/status-check.sh
  - Update docs/architecture diagram and Helm templates to reflect "FluentLogs" / Fluent Bit flow

- Misc:
  - Remove FLUENTD_HOST environment injection from docker-compose.base.yml
  - Cleanup related values.schema.json and values.yaml entries

This consolidates log ingestion under the OpenTelemetry ingest service and removes the separate FluentIngest service and its configuration.
2025-11-07 19:37:31 +00:00
Simon Larsen
b31d1076b8 chore(fluent-ingest): remove /fluent-ingest nginx proxy location from default.conf.template 2025-11-07 19:04:46 +00:00
Simon Larsen
788e82497c chore(fluent-ingest): remove fluent-ingest upstream and Fluentd collector nginx config; route fluentd/syslog endpoints to open-telemetry-ingest 2025-11-07 19:04:32 +00:00
Simon Larsen
3409b0f66e chore(fluent-ingest): remove FluentIngest E2E status check tests 2025-11-07 18:57:06 +00:00
Simon Larsen
cafed35795 chore(fluent-ingest): remove FluentIngest service and all related sources, configs and artifacts 2025-11-07 18:49:09 +00:00
Simon Larsen
69ae1eb310 chore(ci/infra): remove FluentIngest from workflows, charts and compose; bump OpenTelemetryIngest deps
- Remove FluentIngest CI jobs and release/test deploy steps (build.yml, compile.yml, release.yml, test-release.yaml)
- Delete test.fluent-ingest workflow
- Remove Fluent-related env vars/hostnames from config.example.env, docker-compose.base.yml, HelmChart templates and EnvironmentConfig.ts
- Remove fluent-ingest service block from docker-compose.base.yml
- Bump Playwright and TypeORM versions in OpenTelemetryIngest package-lock.json
2025-11-07 18:46:27 +00:00
Nawaz Dhandala
4335c46e3e fix: ProjectPicker use max-w-64 instead of w-64 for responsive container 2025-11-07 18:36:49 +00:00
Simon Larsen
863f1ae82e Merge pull request #2087 from OneUptime/snyk-fix-c8a12071f54add6476bd98c6671cd43d
[Snyk] Security upgrade typeorm from 0.3.20 to 0.3.26
2025-11-07 13:30:31 +00:00
snyk-bot
0755c15886 fix: Common/package.json & Common/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-TYPEORM-13746469
2025-11-07 13:25:43 +00:00
Nawaz Dhandala
72ed43563e Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-07 13:24:21 +00:00
Nawaz Dhandala
15ded6cd46 Merge remote-tracking branch 'origin/snyk-upgrade-4b64a1ce29eed910e23d56c358729865' 2025-11-07 13:24:15 +00:00
Simon Larsen
9e238dc660 Merge pull request #2085 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-07 13:23:08 +00:00
Nawaz Dhandala
9f3ccc7d74 style(exec): type-annotate spawned child as ChildProcess and format spawn call 2025-11-07 13:22:24 +00:00
simlarsen
9e5db22235 chore: npm audit fix 2025-11-07 01:46:40 +00:00
Nawaz Dhandala
e31417c5bf style(exec, nginx, docs): tidy formatting and indentation across Execute.ts, NginxConfigurator.ts, and Telemetry Documentation 2025-11-06 19:41:57 +00:00
Nawaz Dhandala
bf6e97c35d feat(exec): add executeCommandInheritStdio and use it for nginx config test
- Add spawn and SpawnOptions imports and implement Execute.executeCommandInheritStdio
  that runs commands with inherited stdio, logs errors, and rejects on non-zero exit.
- Update NginxConfigurator to run `nginx -t -c /etc/nginx/nginx.conf` via the new
  inherit-stdio helper before reloading nginx.
2025-11-06 19:41:01 +00:00
Nawaz Dhandala
28073ba819 nginx(config): ensure log directory and access/error log files exist before reload 2025-11-06 14:20:13 +00:00
Nawaz Dhandala
4909a5c980 ci(workflows): disable Docker build cache in GitHub Actions
Add --no-cache to docker build and docker buildx commands across build.yml, release.yml and test-release.yaml to force fresh image builds and avoid using cached layers.
2025-11-06 13:54:58 +00:00
Nawaz Dhandala
c4adc24562 ci(release): handle npm publish --dry-run errors and skip if version already published 2025-11-06 13:33:14 +00:00
Nawaz Dhandala
597344483a docs(telemetry): expand common use cases into detailed sections with rsyslog and Fluent Bit examples 2025-11-06 13:21:01 +00:00
Simon Larsen
05d9b79ba2 docs(opentelemetry-ingest): add Syslog testing instructions with RFC5424 curl example 2025-11-06 12:51:56 +00:00
Simon Larsen
af14edb175 docs(telemetry): add "Send Native Syslog" tile to Documentation component 2025-11-06 12:49:36 +00:00
Simon Larsen
0255bd37d0 docs(telemetry): add "Common Use Cases" section to Syslog guide 2025-11-06 12:47:30 +00:00
Simon Larsen
2e6b463bd7 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-06 12:41:33 +00:00
Simon Larsen
95e355ff7d style(opentelemetry-ingest): add RequestHandler/type annotations and simplify parser regexes
- Type setSyslogProductType as RequestHandler
- Import and use ParsedSyslogMessage in parser tests (explicit typed parsed variables)
- Simplify regex usage/formatting in SyslogParser for version and procId matching
- Minor import formatting cleanup
2025-11-06 12:41:29 +00:00
Simon Larsen
cccdcdaf93 style(opentelemetry-ingest): normalize formatting and line-wrapping across ingest code
- Remove extraneous blank line in OTelIngest router
- Reflow imports, function signatures and wrapped expressions in:
  OtelIngestBaseService, SyslogQueueService, SyslogIngestService
- Tidy regex/whitespace and token handling formatting in SyslogParser
- Simplify quoting and compact multiline expectations in SyslogParser tests
- Pure stylistic changes only — no behavioral modifications
2025-11-06 12:34:38 +00:00
Simon Larsen
9256f8b4bd feat(opentelemetry-ingest): add Syslog ingestion API and RFC3164/5424 parsing
- Add new Syslog API (POST /syslog/v1/logs) with product-type middleware and mount it in OpenTelemetryIngest Index
- Move syslog route out of OTelIngest and clean up related imports
- Add parseRfc5424Timestamp and parseRfc3164Timestamp to OneUptimeDate (handles normalization and year rollovers)
- Refactor SyslogParser to use OneUptimeDate parsing helpers and remove duplicated timestamp parsing code
- Update NGINX template to proxy /syslog/v1/logs to open-telemetry-ingest with proper headers/resolver and connection settings
2025-11-06 12:33:36 +00:00
Simon Larsen
b27acbfd38 feat(opentelemetry-ingest): add native Syslog ingestion, parsing, queuing, docs & tests
- Add /syslog/v1/logs endpoint and syslog product-type middleware
- Implement SyslogIngestService: normalize/parse messages, build attributes, batch flush to LogService
- Add robust Syslog parser (RFC5424 & RFC3164) and comprehensive unit tests
- Add TelemetryType.Syslog, SyslogQueueService, and queue handling (enqueue + worker processing)
- Expose OPEN_TELEMETRY_INGEST_SYSLOG_FLUSH_BATCH_SIZE config
- Update Otel ingest router, base service helpers, and ProcessTelemetry worker to support Syslog
- Add documentation page and navigation entry for Syslog telemetry
2025-11-06 12:22:48 +00:00
Nawaz Dhandala
ad9771f222 Merge branch 'release' 2025-11-06 11:55:36 +00:00
Nawaz Dhandala
20a3eab3a0 fix(email): validate using RFC5322 regex and remove redundant Zod check 2025-11-06 11:55:14 +00:00
Nawaz Dhandala
fbe198f0c0 fix(nginx): ensure /var/log/nginx and logs exist; set error_log to /var/log/nginx/error.log
Create /var/log/nginx and touch access.log/error.log in run.sh so nginx -t succeeds before reloads.
Revert nginx.conf error_log to /var/log/nginx/error.log (notice).
2025-11-06 11:34:19 +00:00
Nawaz Dhandala
bb48776e02 fix(nginx): use stderr for error_log to enable container-friendly logging 2025-11-05 21:38:59 +00:00
Nawaz Dhandala
0f92342742 fix(nginx): write error_log to /proc/self/fd/2 for container-friendly logging 2025-11-05 20:33:32 +00:00
Nawaz Dhandala
6ed41b87dd fix(nginx): send error_log to /dev/stderr for container-friendly logging 2025-11-05 19:52:06 +00:00
Nawaz Dhandala
12364415aa ci(release): skip npm publish if version already published; fail on other errors 2025-11-05 19:37:45 +00:00
Nawaz Dhandala
1a3301e715 fix(monitor): normalize disk paths when matching disk metrics (handle backslashes, trailing slashes and root path) 2025-11-05 17:47:33 +00:00
Nawaz Dhandala
8be7b68faf fix(helm): move nodeSelector into e2e CronJob pod template and add per-job override 2025-11-05 16:58:07 +00:00
Nawaz Dhandala
47f9d3914e fix(queue): normalize job IDs (replace ':' with '-') when adding/removing jobs and handling repeatable keys 2025-11-05 15:14:51 +00:00
Nawaz Dhandala
8a1afbe7dc Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-05 13:32:12 +00:00
Simon Larsen
87ac6f3106 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-05 13:23:25 +00:00
Simon Larsen
2df32e4cdd fix(monitor): return null for cached monitor status name instead of undefined 2025-11-05 13:23:23 +00:00
Nawaz Dhandala
2e2adffe17 fix(nginx): use findAllBy and remove LIMIT_MAX when fetching certs for disk write jobs 2025-11-05 13:11:58 +00:00
Nawaz Dhandala
2a15cf8676 fix(acme): adjust ACME challenge route to /acme-challenge/.well-known/:token 2025-11-05 13:02:02 +00:00
Nawaz Dhandala
d249579c1c fix(acme): mount .well-known/acme-challenge router on main router to expose ACME challenge endpoint 2025-11-05 12:52:42 +00:00
Nawaz Dhandala
05681b108b style(monitor): add explicit type annotations and minor formatting cleanup in EvaluationLogList 2025-11-05 12:38:50 +00:00
Nawaz Dhandala
36867a0b8c style(monitor): render criteria message using Alert component in EvaluationLogList 2025-11-05 12:27:12 +00:00
Nawaz Dhandala
0e5a832628 feat(monitor): include incident/alert numbers in evaluation events and show them in UI
- select incidentNumber/alertNumber when loading open incidents/alerts
- attach relatedIncidentNumber / relatedAlertNumber to evaluation events (created/resolved/skipped)
- add number fields to MonitorEvaluationEvent type
- decorate event title/message in EvaluationLogList to include "Incident #N" / "Alert #N"
2025-11-05 12:25:20 +00:00
Nawaz Dhandala
46f69fdde5 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-05 12:14:21 +00:00
Nawaz Dhandala
70e6c0abe1 style(monitor): wrap criteria message and met notice in styled callouts with icons 2025-11-05 12:14:15 +00:00
Simon Larsen
c5938956af docs(readme,helm): add Community vs. Enterprise table to main README; expand Enterprise Edition details in Helm chart README 2025-11-05 12:12:42 +00:00
Simon Larsen
e9bfe74b5d Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-05 12:09:46 +00:00
Simon Larsen
55085a5e6c docs(helm): add Community vs. Enterprise table, document ssl.provision and image.type, remove duplicate Uninstall section 2025-11-05 12:09:42 +00:00
Nawaz Dhandala
9cb48a41e7 style(monitor): restore criteria 'not checked' notice and use ArrowCircleRight icon for events 2025-11-05 12:03:14 +00:00
Nawaz Dhandala
dd8179c0a7 refactor(monitor): remove short-circuit notice and related logic from EvaluationLogList
Remove firstMetCriteriaIndex and shouldShowShortCircuitMessage, and delete the UI block that displayed the "remaining criteria were not evaluated" short-circuit message. Simplifies criteria rendering by removing unused short-circuit handling.
2025-11-05 12:01:04 +00:00
Simon Larsen
f9f84d4104 docs(helm): note Let's Encrypt requires host reachable on ports 80 and 443 2025-11-05 11:58:54 +00:00
Simon Larsen
827663675d refactor(queue): extract BullMQAdapter array and cast to BullBoardQueues for type compatibility 2025-11-05 11:57:48 +00:00
Nawaz Dhandala
0e8d7f2d6b fix(monitor): show short-circuit notice only for first met criteria, remove unused import, and tweak spacing/styles 2025-11-05 11:56:51 +00:00
Nawaz Dhandala
0dd9dfb505 style(monitor): fix JSX conditional formatting in EvaluationLogList 2025-11-05 11:40:36 +00:00
Nawaz Dhandala
b3c68df77e refactor(monitor): centralize persistence of latest monitor payload
Extract persistLatestMonitorPayload and consolidate handling of serverMonitorResponse
and incomingMonitorRequest. Ensure monitor payload updates are saved consistently
(including when there are no steps or no matching monitor step) and before writing logs.
Also clone incomingMonitorRequest via JSON to avoid mutating the original payload and
normalize debug messages.
2025-11-05 11:38:34 +00:00
Nawaz Dhandala
06d0510667 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-11-05 11:34:28 +00:00
Nawaz Dhandala
059b7db474 fix(nginx): narrow ACME challenge location to /.well-known/acme-challenge 2025-11-05 11:30:17 +00:00
Simon Larsen
ff17d990d1 fix(scim): make Create Group idempotent — reuse existing team and return 200 when reused
- Reuse an existing team instead of throwing BadRequest on duplicate group names.
- Create a new team when not found, setting isTeamEditable/isTeamDeleteable and allowing empty groups.
- Ensure member additions operate on the resolved target team.
- Expand team select fields and fetch the team for response; return 201 for newly created teams and 200 when reusing an existing one.
2025-11-05 11:17:28 +00:00
Nawaz Dhandala
2e6658542b delete scim readme 2025-11-05 11:12:27 +00:00
Nawaz Dhandala
04a4d6e4de fix(monitor): show short-circuit note for satisfied criteria and normalize met text color
- Add notice when a criteria is satisfied under FilterCondition.Any to indicate remaining criteria were not evaluated.
- Ensure FilterCondition is imported where used.
- Change "Criteria met" text color in Logs view from green to gray for consistent summary styling.
2025-11-05 10:52:03 +00:00
Nawaz Dhandala
2f595fe490 Merge branch 'monitor-log-summary-update' 2025-11-05 10:49:48 +00:00
Nawaz Dhandala
303554d644 refactor(monitor): group identical criteria filters in EvaluationLogList, aggregate metadata & status; use NORMAL small action buttons 2025-11-05 10:49:24 +00:00
Simon Larsen
2ca45b143d Merge pull request #2083 from OneUptime/monitor-log-summary-update
Monitor log summary update
2025-11-05 10:32:57 +00:00
Nawaz Dhandala
b5722256c5 refactor(email): replace custom RFC5322 regex with Zod email validation and add early falsy check 2025-11-05 09:41:42 +00:00
Nawaz Dhandala
0d231a6132 chore(eslint): enable no-control-regex and tidy rule formatting 2025-11-05 09:38:26 +00:00
Simon Larsen
ecc1d841e2 Merge pull request #2080 from OneUptime/snyk-upgrade-ad5a46fc6886b9d33f00f95d68951c86
[Snyk] Upgrade zod from 3.25.30 to 3.25.76
2025-11-05 09:34:42 +00:00
Simon Larsen
cddf534449 Merge pull request #2081 from OneUptime/snyk-upgrade-58044c8091e4244c9346f20e7029dcbd
[Snyk] Upgrade @opentelemetry/api-logs from 0.52.1 to 0.206.0
2025-11-05 09:34:36 +00:00
Simon Larsen
a4ba662211 Merge pull request #2078 from OneUptime/snyk-upgrade-a266987590b01e591eec631cb95143b3
[Snyk] Upgrade bullmq from 5.26.2 to 5.61.0
2025-11-05 09:34:20 +00:00
Simon Larsen
604bc33fb3 Merge pull request #2077 from OneUptime/snyk-upgrade-60fa724635c3f357d78809a2791dae5d
[Snyk] Upgrade posthog-js from 1.186.3 to 1.275.3
2025-11-05 09:34:12 +00:00
Simon Larsen
a1ae1bee89 Merge pull request #2082 from OneUptime/monitor-log-summary-update
Monitor log summary update
2025-11-05 09:33:51 +00:00
Nawaz Dhandala
775b8846c7 refactor(monitor): tidy formatting, consolidate imports and add type annotations
- Normalize whitespace/indentation and reformat several monitor utilities and message builders
- Consolidate and reorder imports (including MonitorEvaluationSummary) across evaluator/data-extractor modules
- Add explicit types (MonitorStatus, BasicDiskMetrics) and tighten type annotations in observation/resource code
- Minor cleanups to conditional formatting in dashboard components (EvaluationLogList, SummaryInfo)
2025-11-05 09:33:29 +00:00
Nawaz Dhandala
3837208023 refactor(monitor): split MonitorCriteriaMessageBuilder into smaller responsibility-focused modules
Move expectation, observation, data-extraction and formatting logic out of the large
MonitorCriteriaMessageBuilder into new classes:
- MonitorCriteriaExpectationBuilder
- MonitorCriteriaObservationBuilder
- MonitorCriteriaDataExtractor
- MonitorCriteriaMessageFormatter

Keep MonitorCriteriaMessageBuilder slim: it now orchestrates message construction and
delegates description/formatting responsibilities to the new modules.
2025-11-05 09:23:14 +00:00
Nawaz Dhandala
b45910a22e refactor(monitor): extract criteria evaluation, message builder, metrics & log utils from MonitorResource
- Move criteria processing into MonitorCriteriaEvaluator
- Move filter/observation message logic into MonitorCriteriaMessageBuilder
- Move monitor metrics logic into MonitorMetricUtil
- Move monitor log persistence into MonitorLogUtil
- Replace large in-file implementations in MonitorResource with calls to the new utilities to reduce complexity and improve separation of concerns
2025-11-05 08:59:14 +00:00
Nawaz Dhandala
c787d7eca0 refactor(monitor): format compare messages and add value formatting helpers 2025-11-05 08:38:04 +00:00
snyk-bot
9771c4cd69 fix: upgrade @opentelemetry/api-logs from 0.52.1 to 0.206.0
Snyk has created this PR to upgrade @opentelemetry/api-logs from 0.52.1 to 0.206.0.

See this package in npm:
@opentelemetry/api-logs

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-04 23:28:08 +00:00
snyk-bot
4471d6bec4 fix: upgrade zod from 3.25.30 to 3.25.76
Snyk has created this PR to upgrade zod from 3.25.30 to 3.25.76.

See this package in npm:
zod

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-04 23:28:03 +00:00
snyk-bot
95e24d9ae0 fix: upgrade playwright from 1.55.1 to 1.56.0
Snyk has created this PR to upgrade playwright from 1.55.1 to 1.56.0.

See this package in npm:
playwright

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-04 23:27:58 +00:00
snyk-bot
f8a7330f79 fix: upgrade bullmq from 5.26.2 to 5.61.0
Snyk has created this PR to upgrade bullmq from 5.26.2 to 5.61.0.

See this package in npm:
bullmq

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-04 23:27:53 +00:00
snyk-bot
ae177c920f fix: upgrade posthog-js from 1.186.3 to 1.275.3
Snyk has created this PR to upgrade posthog-js from 1.186.3 to 1.275.3.

See this package in npm:
posthog-js

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f6446ec8-d441-487e-b58f-38373430e213?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-04 23:27:47 +00:00
Nawaz Dhandala
9faa38454d refactor(monitor): improve criteria filter messages and add rich observation descriptions
- Replace ad-hoc success/failure messaging with buildCriteriaFilterMessage / buildCriteriaFilterFailureMessage
- Add detailed describe*Observation helpers for many CheckOn types (response, headers, body, isOnline, timeouts, incoming requests, CPU/memory/disk, processes, SSL, synthetic/custom code, metrics, logs, spans)
- Introduce utility helpers: formatNumber, formatPercentage, formatBytes, formatList, formatSnippet, describeProcesses, computeDiskUsagePercent, summarizeNumericSeries, formatResultValue, and various response extractors
- Enhance metric value extraction to select correct aggregated result by alias and return summarized numeric series
- Wire new messaging into MonitorResource evaluation flow (use filterMessage everywhere)
- Import and use FilterType and ServerProcess types plus several monitor response types
- Small fix in ServerMonitorCriteria: find disk metric and prefer percentUsed (fall back to percentFree) when computing disk usage percent
2025-11-04 22:39:51 +00:00
Nawaz Dhandala
a4198ec409 refactor(monitor): consolidate evaluation logs for probe-based monitors
Compute a probable evaluationSummary from props.evaluationSummary or the first probe response that has one, remove per-probe EvaluationLogList, and render evaluation logs once for probable (probe) monitors to avoid duplicated log lists.
2025-11-04 22:11:58 +00:00
Nawaz Dhandala
24d184debf feat(monitor, dashboard): resolve monitor status names in events and add view buttons for incidents/alerts
Cache and fetch monitor status names in MonitorResource and include readable status names in evaluationSummary events. Add action buttons in EvaluationLogList to navigate to related incident/alert views.
2025-11-04 22:08:08 +00:00
Nawaz Dhandala
f307c904b0 fix(monitor): persist monitor log on early exits and add input guards to saveMonitorLog 2025-11-04 21:31:54 +00:00
Nawaz Dhandala
2d20b7fd13 refactor(monitor): add explicit typings for render functions and evaluationSummary; simplify catch
Add explicit function type annotations for renderCriteriaResult, renderEvent and renderEvaluationLogs in SummaryView to improve type safety and readability. Also type the evaluationSummary variable in Monitor view and simplify the catch block by removing the unused error parameter.
2025-11-04 21:11:18 +00:00
Nawaz Dhandala
fd738b23d0 refactor(monitor, dashboard): normalize multiline formatting and simplify conditional logic
- Normalize multiline/inline formatting in MonitorResource.ts, EvaluationLogList.tsx and Logs.tsx
- Simplify nested else to else-if for FilterCondition.All in MonitorResource
- Minor JSX spacing and map return cleanup for clearer readability
2025-11-04 21:09:02 +00:00
Simon Larsen
deffa6489d feat(monitor): fetch and surface evaluationSummary from MonitorLog in Monitor view and Logs
- Add AnalyticsModelAPI call in Monitor view to load the latest MonitorLog and extract evaluationSummary into component state.
- Pass latest evaluationSummary to Summary component in Monitor view.
- Add Evaluation Outcome column to Monitor Logs table to show criteria met / no criteria / not recorded.
- Pass evaluationSummary into the Logs modal's SummaryInfo.
- Import MonitorEvaluationSummary types and MonitorLog analytics model.
2025-11-04 21:00:14 +00:00
Simon Larsen
e5f1d5553e feat(monitor): add MonitorEvaluationSummary and evaluation logs
- introduce MonitorEvaluationSummary type and related result/event types
- thread evaluationSummary through probe/monitor response types and probe ingest flow
- initialize and populate evaluationSummary in MonitorResource (criteriaResults, events)
- record events for criteria evaluation, monitor status changes, incident/alert create/skip/resolve
- update criteria filter evaluation to produce structured filter results and messages
- add EvaluationLogList UI and wire evaluationSummary into Summary/SummaryInfo views
- minor Date utility usages to timestamp events
2025-11-04 20:58:18 +00:00
Nawaz Dhandala
d2ee3c5409 refactor(admin-dashboard, nginx): normalize multiline formatting in Serve.ts, NginxConfigurator.ts and WriteServerCertToDisk.ts 2025-11-04 20:39:53 +00:00
Nawaz Dhandala
9a1ecd7fe0 fix(admin-dashboard): clarify NotAuthorizedException messages in Serve.ts
Replace generic "Only master admins can access this app." with
"Unauthorized: Only master admins can access the admin dashboard." to
provide clearer, more informative authorization errors.
2025-11-04 20:39:14 +00:00
Simon Larsen
b41d8ab5ab Merge pull request #2076 from OneUptime/ssl-load
Ssl load
2025-11-04 20:36:03 +00:00
Nawaz Dhandala
b809e1c43b refactor(analytics): extract includesValues and simplify numeric-array detection in Statement.toColumnType 2025-11-04 20:35:33 +00:00
Nawaz Dhandala
743f8721f3 fix(helm): correct YAML indentation for SERVER_ADMIN_DASHBOARD_HOSTNAME in _helpers.tpl 2025-11-04 20:06:40 +00:00
Simon Larsen
c0aa2b7905 Merge pull request #2075 from OneUptime/ssl-load
Ssl load
2025-11-04 19:56:35 +00:00
Nawaz Dhandala
4ac5819e6a fix(nginx): generate placeholder certs and guard HTTPS directives; test config after envsubst
Add ensure_placeholder_certificate to envsubst-on-templates.sh to generate a temporary self-signed cert when PRIMARY_DOMAIN is set but cert files are missing. Only export SSL listen/certificate directives if certs (real or placeholder) are available; otherwise disable HTTPS directives and emit warnings. Simplify PRIMARY_DOMAIN logging.

Also run "nginx -t" immediately after running the envsubst script in NginxConfigurator to validate generated config before attempting reload.
2025-11-04 19:55:22 +00:00
Nawaz Dhandala
158663c44b fix(nginx): only write/reload when certs change; derive primary domain and guard SSL directives
- WriteServerCertToDisk: read existing cert/key from disk and compare with DB values; skip writing when unchanged. After writing, run envsubst-on-templates.sh and reload nginx with try/catch and logging.
- envsubst-on-templates.sh: derive PRIMARY_DOMAIN from HOST when not set, compute cert/key paths, and only export PROVISION_SSL_* directives when certificate files exist (otherwise clear directives and log).
2025-11-04 19:44:55 +00:00
Simon Larsen
3d2bcfa579 refactor(admin): add types for ensureMasterAdminAccess and JSONWebToken decode result 2025-11-04 19:21:07 +00:00
Simon Larsen
21984c8684 chore(docker-compose): consolidate common-ui/server anchors into common-runtime-variables
Replace <<: *common-ui-variables and <<: *common-server-variables with <<: *common-runtime-variables across services and remove the IS_SERVER flag from the server anchor.
2025-11-04 19:18:28 +00:00
Simon Larsen
ad63d18f0a fix(helm): replace runtime env include with oneuptime.env.oneuptimeSecret in isolated-vm template 2025-11-04 19:17:45 +00:00
Simon Larsen
e5af008079 chore(helm): unify runtime env template and update deployments
Replace separate commonServer/commonUi and oneuptimeSecret includes with a single
oneuptime.env.runtime include across deployments. Move oneuptimeSecret into the
common env where appropriate and remove SERVER_ADMIN_DASHBOARD_HOSTNAME.
Update all affected templates to use the new runtime include.
2025-11-04 19:14:27 +00:00
Simon Larsen
3e72b2a9a4 fix(admin): restrict AdminDashboard to master admins and guard index render
Add ensureMasterAdminAccess in AdminDashboard/Serve.ts to decode JWT, validate
isMasterAdmin and return a NotAuthorized response when access is not allowed.
Wire this function into App.init as getVariablesToRenderIndexPage so the admin
frontend is protected.

Also add a res.headersSent guard in StartServer.ts to skip rendering the index
page if a response was already sent, avoiding duplicate responses.
2025-11-04 18:28:10 +00:00
Nawaz Dhandala
6d66c6c369 ci(test-release): replace MCP server Docker builds with Worker image builds
Update test-release workflow to build and push Worker images (./Worker/Dockerfile) and adjust tags (including enterprise variants) instead of the previous MCP server build steps.
2025-11-04 17:58:41 +00:00
Nawaz Dhandala
9544dc2a6c chore(env): use explicit callback body for prefix startsWith check in getFrontendEnvVars 2025-11-04 16:47:07 +00:00
Simon Larsen
a22e3b63e0 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-11-04 16:08:53 +00:00
Simon Larsen
5f1f0cde4a fix(env): restrict frontend env vars and use sanitized getFrontendEnvVars in env.js
Add getFrontendEnvVars with an allow-list and allowed prefixes to EnvironmentConfig to avoid exposing the entire process.env to the browser. Update StartServer to use getFrontendEnvVars when rendering /env.js and embed the serialized env object directly.
2025-11-04 16:08:47 +00:00
Nawaz Dhandala
3025880d6d chore(nginx): move conditional SSL listen/certificate directives into envsubst variables 2025-11-04 15:59:14 +00:00
Nawaz Dhandala
00994b56c5 chore(values.yaml): relocate image configuration next to ssl and remove duplicate block 2025-11-04 15:35:27 +00:00
Nawaz Dhandala
d56fd8bf69 fix(domain): disallow creating verified domains for non-root users; require post-create verification 2025-11-04 15:09:41 +00:00
Nawaz Dhandala
1229d5d204 style(domain): normalize whitespace and formatting in DomainService.ts 2025-11-04 12:18:14 +00:00
Nawaz Dhandala
c4f1f4e711 fix(domain): allow setting deletedByUserId on create for project roles 2025-11-04 12:17:33 +00:00
Nawaz Dhandala
7b8986b011 fix(domain): require projectId for verification and verify TXT for all project domains; set isVerified false for non-root creates
- set createBy.data.isVerified = false for non-root creations
- require projectId (from query.projectId or props.tenantId) when verifying domains
- query by projectId and other filters, use LIMIT_PER_PROJECT and iterate results to verify TXT records
- add missing imports (LIMIT_PER_PROJECT, ObjectID, FindWhere) and improve error handling/messages
2025-11-04 12:14:51 +00:00
Nawaz Dhandala
46e6176e6a refactor(queue): use Redis.getRedisOptions() for BullMQ connections; remove direct Redis env usage 2025-11-04 11:53:43 +00:00
Simon Larsen
61f9572956 Merge pull request #2073 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-04 10:48:58 +00:00
simlarsen
3adc6901da chore: npm audit fix 2025-11-04 01:46:29 +00:00
Simon Larsen
d7f1bfb52a Merge pull request #2072 from OneUptime/core-ssl
refactor(helm): evaluate PROVISION_SSL once in oneuptime.env.common a…
2025-11-03 22:17:55 +00:00
Nawaz Dhandala
53968e681c refactor(helm): evaluate PROVISION_SSL once in oneuptime.env.common and replace dig call 2025-11-03 22:14:19 +00:00
Simon Larsen
3b8c854744 Merge pull request #2071 from OneUptime/core-ssl
Core ssl
2025-11-03 22:01:25 +00:00
Nawaz Dhandala
669ed2580c refactor(coressl,nginx): normalize formatting in WriteServerCertToDisk job
Wrap logger calls and LocalFile.write arguments for consistent multiline formatting.
2025-11-03 22:00:24 +00:00
Nawaz Dhandala
2f29c2e24c refactor(helm): move provisionSSL under ssl.provision and update templates/schema
Nest the top-level provisionSSL into ssl.provision in values.yaml and values.schema.json,
and update _helpers.tpl to read the new path (using default false via dig) so PROVISION_SSL
env is derived from ssl.provision.
2025-11-03 21:56:17 +00:00
Nawaz Dhandala
f0a2f454e2 feat(config,docker-compose): add PROVISION_SSL env and document Let's Encrypt provisioning
Expose PROVISION_SSL in docker-compose common variables and update config.example.env docs to explain automatic ACME/Let's Encrypt TLS provisioning and reverse-proxy alternative.
2025-11-03 21:51:14 +00:00
Nawaz Dhandala
d8206e12de feat(nginx): add ServerCerts volume/mount and enable conditional primary-domain SSL provisioning
- add server-certs emptyDir volume and mount to nginx Deployment
- update default.conf.template to conditionally listen on 7850 and use /etc/nginx/certs/ServerCerts/${PRIMARY_DOMAIN}.crt/.key when PROVISION_SSL is set
- enhance run.sh to export PRIMARY_DOMAIN, temporarily adjust PROVISION_SSL for envsubst, and restore original PROVISION_SSL afterwards
2025-11-03 21:48:15 +00:00
Nawaz Dhandala
f84434ada4 feat(nginx,coressl): add job to write primary host TLS certificate to disk and initialize it 2025-11-03 21:27:22 +00:00
Nawaz Dhandala
d5fbe0443e refactor(acme,coressl): normalize formatting for AcmeChallenge routing, handler signature, and provisioning log 2025-11-03 21:12:46 +00:00
Nawaz Dhandala
2732cd65ed feat(acme): add ACME HTTP-01 challenge routing and nginx proxy
- Refactor AcmeChallengeAPI into a BaseAPI-backed class that exposes a well-known router.
- Add CrudApiEndpoint(Route("/acme-challenge")) to AcmeChallenge model.
- Register AcmeChallengeAPI router in BaseAPIFeatureSet via (new AcmeChallengeAPI).getRouter().
- Add nginx location /.well-known to proxy ACME challenge requests to /api/acme-challenge/.well-known with proper headers, resolver and websocket support.
2025-11-03 21:12:01 +00:00
Nawaz Dhandala
7624523446 feat(api): add ACME HTTP-01 challenge endpoint and register AcmeChallengeAPI 2025-11-03 20:59:02 +00:00
Nawaz Dhandala
5851286548 feat(coressl): add automated Let's Encrypt provisioning for primary host
- add ProvisionPrimaryDomain worker job to order/renew ACME certificates for the HOST
- register job import in Worker Routes
- add ProvisionSsl env flag in Common/Server/EnvironmentConfig
- expose PROVISION_SSL in Helm chart (values.yaml, values.schema.json, _helpers.tpl)
2025-11-03 20:48:07 +00:00
Simon Larsen
123d9b07bc Merge pull request #2067 from OneUptime/service-catalog-pages
Service catalog pages
2025-11-03 19:50:54 +00:00
Nawaz Dhandala
9edc6ac428 refactor(metrics): normalize type annotations and formatting in MetricExplorer/MetricQuery/MetricView 2025-11-03 19:47:27 +00:00
Nawaz Dhandala
72fc633bf1 refactor(metrics): treat metric query alias as meaningful so alias-only queries are preserved in URL/state 2025-11-03 19:44:21 +00:00
Nawaz Dhandala
3264322054 refactor(metrics): support metric query alias in URL/state
- add MetricQueryAliasFromUrl type
- populate initial metric alias fields from parsed URL into initial query configs
- include alias when building metricQueries for the URL and when parsing metricQueries from the URL
- add sanitizeAlias and buildAliasFromMetricAliasData helpers to validate/serialize alias fields
2025-11-03 19:41:48 +00:00
Nawaz Dhandala
d8fedc0b19 refactor(filters,metrics): default to showing advanced filters and optimize metric fetching
- add showAdvancedFiltersByDefault prop to FiltersForm and use it to initialize advanced filter visibility
- MetricQuery: default showAdvancedFilters to true, pass showAdvancedFiltersByDefault to FiltersForm and call onAdvancedFiltersToggle once on mount
- MetricView: introduce getFetchRelevantState and lastFetchSnapshotRef; only fetch aggregated results when relevant state (start/end dates or queryConfigs) actually changes
2025-11-03 19:33:12 +00:00
Nawaz Dhandala
fc7cc5fe7f Merge branch 'master' into service-catalog-pages 2025-11-03 18:55:32 +00:00
Nawaz Dhandala
5b4eb72521 refactor(modal,edition-label): make Modal onSubmit optional and centralize modal props
- Make Modal.onSubmit and ModalFooter.onSubmit optional and use safe optional chaining when invoking
- Extract modalSubmitButtonText, modalOnSubmit, modalIsLoading, and modalDisableSubmitButton in EditionLabel to simplify JSX and reduce inline conditional logic
2025-11-03 18:54:27 +00:00
Nawaz Dhandala
d84cfe9b09 refactor(enterprise): register EnterpriseLicense migration and clean up license validation/env config; refactor(edition-label): adjust alert formatting 2025-11-03 18:35:00 +00:00
Nawaz Dhandala
0e8926a786 refactor(edition-label): update edition labels and streamline license validation UI
- Change "Enterprise Edition (Verified)" -> "Enterprise Edition"
- Change "Enterprise Edition (License Needed)" -> "Enterprise Edition (License Required)"
- Render success Alert without extra wrapper
- Only show license input, validation error and explanatory text when license is not valid
2025-11-03 18:33:33 +00:00
Nawaz Dhandala
12ff3062de feat(enterprise): add server license validation endpoint and integrate UI
- Add POST /global-config/license handler to validate enterprise license via
  EnterpriseLicenseValidationUrl and store results in GlobalConfig.
- Introduce EnterpriseLicenseValidationUrl in EnvironmentConfig.
- Update EditionLabel to call /global-config/license (POST) for validation,
  remove direct ModelAPI update and ObjectID usage, and replace inline messages
  with Alert component. Minor styling and import cleanup.
2025-11-03 18:21:23 +00:00
Simon Larsen
30aad2866f fix(enterprise): remove oneuptime.com host restriction from license validation (comment out Host import and serverHost check) 2025-11-03 15:25:53 +00:00
Simon Larsen
3de636ab9e refactor(edition-label): remove refresh flow and unify license validation
- Remove isRefreshing state and handleRefresh function; use isValidating for all validation/loading logic
- Make modal submit perform license validation (label "Validate License") and derive isLoading/disable state from licenseKeyInput, isValidating, and isConfigLoading
- Change retry button style to DANGER
- Expand enterprise features copy ("Audit logs and many more enterprise-focused features.")
- Reorder and simplify dialog markup (remove in-body Validate button)
2025-11-03 15:10:58 +00:00
Simon Larsen
a1bf9cbaae feat(enterprise): add EnterpriseLicense DB migration (1762181014879) and register in migrations index 2025-11-03 14:46:00 +00:00
Simon Larsen
12c800b81f refactor(edition-label): add explicit types for ref and callbacks, tidy formatting and normalize config.example.env newline 2025-11-03 14:29:36 +00:00
Simon Larsen
677e687662 fix(enterprise): restrict enterprise license validation to oneuptime.com and import Host 2025-11-03 14:16:45 +00:00
Simon Larsen
93719d67be refactor(link): use ReactNode for children and handle numeric children 2025-11-03 14:12:29 +00:00
Simon Larsen
7d23209198 chore(format): tidy formatting in GlobalConfig, EditionLabel, and Config 2025-11-03 14:09:09 +00:00
Simon Larsen
4461127a36 feat(enterprise): add /global-config/license endpoint and make EditionLabel fetch license via API
- Add GET /global-config/license that returns enterprise license fields (companyName, expiresAt, licenseKey, token).
- Update EditionLabel to use APP_API_URL and fetch license from the new endpoint instead of ModelAPI.getItem.
- Replace hard-coded validation URL with APP_API_URL-based URL.
2025-11-03 14:08:20 +00:00
Simon Larsen
8326bf2c9e chore(edition-label): no-op commit (no code changes) 2025-11-03 14:03:06 +00:00
Simon Larsen
be9d2f6beb feat(enterprise): add enterprise license fields to GlobalConfig
Add enterpriseCompanyName, enterpriseLicenseKey, enterpriseLicenseExpiresAt and enterpriseLicenseToken to store validated enterprise license metadata.
2025-11-03 14:01:24 +00:00
Simon Larsen
214dae6204 feat(enterprise): add license validation endpoint to EnterpriseLicense API
Add POST /.../validate route that verifies license key, checks expiration,
and returns license info with a signed JWT token.
2025-11-03 13:40:09 +00:00
Simon Larsen
71c845d94e feat(enterprise): add EnterpriseLicense API
Add EnterpriseLicenseAPI class that extends BaseAPI to expose CRUD operations
for the EnterpriseLicense model using EnterpriseLicenseService.
2025-11-03 13:32:52 +00:00
Simon Larsen
87d709dd05 feat(enterprise): add EnterpriseLicense model, service and API; register model/service and add IsEnterpriseEdition env flag 2025-11-03 12:19:55 +00:00
Simon Larsen
25332f99fd chore(helm): add IS_ENTERPRISE_EDITION env var to oneuptime.env.common 2025-11-03 11:43:18 +00:00
Simon Larsen
1ac6e71f7e chore(config,docker,ci,ui): rename IS_ENTERPRISE to IS_ENTERPRISE_EDITION across env, Dockerfiles, compose and workflows 2025-11-03 11:25:12 +00:00
Simon Larsen
f1efd65ada Merge pull request #2070 from OneUptime/snyk-upgrade-ecc4458d6ea952fff3607671e1a1fb1d
[Snyk] Upgrade react-router-dom from 6.23.1 to 6.30.1
2025-11-03 08:51:04 +00:00
snyk-bot
bc338f41c7 fix: upgrade react-router-dom from 6.23.1 to 6.30.1
Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.30.1.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/40b17bc5-1bd4-48b1-88f1-5b4dc1400e80?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-11-02 10:11:06 +00:00
Simon Larsen
39153735b5 Merge pull request #2069 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-11-02 09:18:55 +00:00
simlarsen
ae9a78f1f4 chore: npm audit fix 2025-11-01 01:49:26 +00:00
Nawaz Dhandala
f224ad6092 fix(ui): display correct feature list and icon for Community Edition
- Map communityFeatures (not enterpriseFeatures) in the Community Edition panel
- Replace wrong/Danger icon with Check and add muted gray styling
2025-10-31 18:14:32 +00:00
Nawaz Dhandala
1abb8bc83f style(ui): reformat EditionLabel and trim whitespace in Dashboard Footer
- Reflow imports, map callbacks and wrapped long strings in EditionLabel for improved readability.
- Remove stray trailing space in Dashboard Footer link.
2025-10-31 18:07:23 +00:00
Nawaz Dhandala
46704b7c5a fix(ui): correct IconProp relative import path in EditionLabel 2025-10-31 16:48:17 +00:00
Nawaz Dhandala
143d91ceab feat(ui): revamp EditionLabel to use shared enterpriseFeatures and render feature lists with icons 2025-10-31 16:46:28 +00:00
Nawaz Dhandala
ea58cacd1b feat(ui): revamp EditionLabel and allow Footer content links
- Update EditionLabel UI: new button styling, status indicator, compact CTA text
- Expand modal content with Community vs Enterprise comparison and use enterprise demo URL
- Pass ModalWidth to modal and refine primary action behavior
- Extend Footer types to accept ReactNode content and render content links
- Embed EditionLabel into Dashboard footer links
2025-10-31 16:43:07 +00:00
Nawaz Dhandala
5e19849ac8 feat(ui): add EditionLabel and expose IS_ENTERPRISE across apps
- Add EditionLabel component to Common UI to show current edition and info modal
- Show edition label in Login page, Dashboard header and Footer
- Add IS_ENTERPRISE env var to config.example.env and export in Common UI Config
- Propagate IS_ENTERPRISE into docker-compose.base.yml service envs
2025-10-31 15:47:12 +00:00
Nawaz Dhandala
f7c05645a9 feat(helm-chart): add image helper and support enterprise image type
- Add Helm helpers oneuptime.image.tag and oneuptime.image to centralize image name/tag formatting
- If image.type is "enterprise-edition" and tag doesn't already contain "enterprise", prefix tag with "enterprise-"
- Replace inline image printf calls with include "oneuptime.image" across deployments, cronjob and tests
- Add image.type to values.schema.json and set default type to "community-edition" in values.yaml
2025-10-31 15:17:41 +00:00
Nawaz Dhandala
1c1a48b78f chore(ci): build/publish enterprise image variants and add IS_ENTERPRISE arg to Dockerfiles 2025-10-31 14:49:07 +00:00
Nawaz Dhandala
13860be56d chore(helm-chart): remove cert-manager integration from oneuptime chart
- Remove cert-manager dependency from Chart.yaml and update Chart.lock (digest/timestamp)
- Delete bundled cert-manager chart (charts/cert-manager-v1.18.2.tgz)
- Remove ClusterIssuer template and cert-manager/Let's Encrypt-related ingress annotations and secret handling
- Remove cert-manager and certManagerLetsEncrypt entries from values.schema.json and values.yaml
- Clean up README to remove Cert-Manager / Let's Encrypt documentation and references
2025-10-30 21:52:19 +00:00
Nawaz Dhandala
38c29664ea refactor(metrics): strengthen typings and clean up MetricExplorer
- Add explicit types for initialQueryConfigs (MetricQueryConfigData) and map callbacks
- Use FilterData<MetricsQuery> and explicit Record<string, unknown> for safer access
- Type lastSerializedStateRef as React.MutableRefObject<string>
- Simplify catch blocks (remove unused error variables) and tidy parsing/sanitization logic
- Minor formatting and type-safe attribute sanitization improvements
2025-10-30 21:36:44 +00:00
Nawaz Dhandala
df1507b314 feat(metrics): persist multiple metric queries & time range in URL
- Refactor MetricExplorer to parse/serialize a metricQueries URL param (array of metricName, attributes, aggregationType) and startTime/endTime.
- Add helpers to sanitize attributes, map aggregation types, and build metric query state.
- Update MetricsTable to generate metricQueries payload (with attributes and aggregationType) when navigating to metric view.
- Minor JSX/formatting cleanup.
2025-10-30 21:33:30 +00:00
Nawaz Dhandala
65c999b5fc feat(metrics): support filtering by multiple telemetry services
Replace telemetryServiceId/telemetryServiceName props with telemetryServiceIds array.
Update MetricsTable to accept multiple IDs, construct view route when exactly one
telemetry service is selected, query with Includes for multiple services, and add
select/filters/column to show telemetry service info. Update ServiceCatalog and
Telemetry Service Metrics pages to pass telemetryServiceIds and remove redundant
single-service fetching/state.
2025-10-30 20:32:35 +00:00
Nawaz Dhandala
803d0436ca feat(service-catalog): reorganize side menu and tidy view components
- Move "Telemetry Services" into the Resources section and introduce an Operations
  section for Alerts and Incidents; update corresponding icons and route targets.
- Reformat FunctionComponent type annotations and async fetch function bodies
  in Alerts, Logs, Traces and Metrics for consistent indentation and readability.
- Minor formatting cleanup for pageRoute/path prop in ServiceCatalogRoutes.
2025-10-30 20:12:52 +00:00
Nawaz Dhandala
b98e7f13a5 feat(service-catalog): add Alerts, Logs, Traces and Metrics pages, routes and menu
- Add new ServiceCatalog view pages: Alerts.tsx, Logs.tsx, Traces.tsx, Metrics.tsx
  (fetch monitors/telemetry service ids and render respective tables/viewers).
- Register lazy routes and PageRoute entries in ServiceCatalogRoutes for alerts,
  logs, traces and metrics.
- Extend PageMap and RouteMap with new keys/paths and Route entries.
- Update SideMenu to include Alerts under Operations and Logs/Traces/Metrics under
  a Telemetry section.
- Add breadcrumbs entries for the new service catalog pages.
2025-10-30 20:08:52 +00:00
Simon Larsen
0785f11abe Merge pull request #2066 from OneUptime/live-logs
Live logs
2025-10-30 18:58:45 +00:00
Nawaz Dhandala
f0d9f7c594 chore(incidents): add IncidentUpdatePayload type and use it for updatedIncidentData 2025-10-30 18:58:19 +00:00
Nawaz Dhandala
dc9463f73d chore(logs-viewer,logs): add explicit types for liveRequestInFlight, fetchItems and handleLiveToggle 2025-10-30 18:45:54 +00:00
Nawaz Dhandala
37c8e8b6b6 chore(logs-viewer,logs): tweak LiveLogsToggle styling for improved contrast
- add bg-white/90 and backdrop-blur to base classes
- update active/inactive border, text and hover classes
- adjust indicator colors and make "Live" label font-semibold
2025-10-30 18:42:41 +00:00
Nawaz Dhandala
763dfaa1c9 chore(logs-viewer,logs): track live-updating state and disable live toggle during live fetches 2025-10-30 18:37:55 +00:00
Nawaz Dhandala
c2e0d402d5 chore(logs-viewer,logs): remove live updating indicator and tooltip; simplify LiveLogsToggle API
- Remove isUpdating and tooltip from LiveLogsToggle props and types
- Drop Tooltip import and spinner markup; always return button content
- Remove isLiveUpdating state and related updates from Dashboard LogsViewer
- Simplify liveOptions to only pass isLive and onToggle
2025-10-30 18:34:48 +00:00
Simon Larsen
cdc1526fbf chore(logs-viewer,logs): normalize formatting - inline LiveLogsToggle JSX and reformat catch callback 2025-10-30 18:20:10 +00:00
Simon Larsen
13ebd34e8f feat(logs-viewer): add live logs toggle and realtime refresh support
- Add LiveLogsOptions type and LiveLogsToggle component.
- Wire liveOptions through LogsViewer -> LogsViewerToolbar to render the live toggle.
- Update Dashboard LogsViewer to support live mode:
  - add isLiveEnabled, isLiveUpdating state and liveRequestInFlight ref.
  - change fetchItems to accept skipLoadingState to perform lightweight live refreshes (sets isLiveUpdating instead of full loader).
  - poll every 10s when live is enabled (only on page 1, sorted by time desc) and use skipLoadingState for background refreshes.
  - integrate with realtime listener and avoid overlapping live requests.
  - add handleLiveToggle to enforce page/sort when enabling and to stop live updates when disabling.
- Automatically disable live mode when user changes filters, page, or sort in ways that conflict with live behavior.
- Minor imports/exports and typing adjustments.
2025-10-30 18:17:17 +00:00
Nawaz Dhandala
3b97c23039 Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2025-10-30 16:45:43 +00:00
Simon Larsen
7c15424565 Merge pull request #2064 from OneUptime/incident-postmortem
Incident postmortem
2025-10-30 16:45:20 +00:00
Nawaz Dhandala
6817443d9a chore: normalize formatting and comment style across migrations, jobs and postmortem UI
- Reformat MigrationName1761834523183.ts SQL queries to multi-line strings and consistent quoting
- Add trailing comma to migrations Index export
- Convert inline // comments to /* ... */ block comments in KeepCurrentStateConsistent jobs (Alert, Incident, Monitor, ScheduledMaintenance) for consistency
- Minor refactor in IncidentPostmortem: expand setRefreshToggle updater to an explicit return
2025-10-30 16:44:54 +00:00
Nawaz Dhandala
f40a6395a6 feat(incident): set create/edit modal width for incident postmortem templates 2025-10-30 16:42:25 +00:00
Nawaz Dhandala
81eb735aab feat(incident): extract postmortem form fields and apply templates via edit modal 2025-10-30 16:39:25 +00:00
Nawaz Dhandala
923339710b feat(incident): add migration to create IncidentPostmortemTemplate and add postmortemNote to Incident 2025-10-30 14:30:56 +00:00
Nawaz Dhandala
1f9ec3011c feat(incident): add postmortem UI, templates, routes and breadcrumbs
- Add Incident Postmortem page to incident view with:
  - Postmortem note CardModelDetail (edit/save)
  - "Apply Template" modal to pick and apply a postmortem template
- Add Settings pages for postmortem templates:
  - IncidentPostmortemTemplates (list/create)
  - IncidentPostmortemTemplateView (view/edit/delete)
- Wire up routes, route map, page map and side menu links for postmortem views
- Add breadcrumbs entries for incident postmortem and settings pages
- Minor cleanup/formatting tweaks in IncidentPostmortemTemplate model and IncidentService
2025-10-30 14:17:24 +00:00
Nawaz Dhandala
668093b09c feat(incident): add postmortem template & postmortem note feed support
- add IncidentPostmortemTemplate model and IncidentPostmortemTemplateService
- expose model in Common/Models/DatabaseModels index and register API route in BaseAPI
- add postmortemNote column to Incident model
- add IncidentFeedEventType.PostmortemNote enum value
- emit IncidentFeed item when postmortemNote is added/updated/cleared (IncidentService)
- update Dashboard IncidentFeed to show Book icon for PostmortemNote events
2025-10-30 12:27:13 +00:00
Simon Larsen
f39f51e8ee Merge pull request #2063 from OneUptime/master
chore(jobs): comment out KeepCurrentStateConsistent implementations a…
2025-10-30 12:08:05 +00:00
Nawaz Dhandala
0bff616ca9 chore(jobs): comment out KeepCurrentStateConsistent implementations and unused imports for Alert, Incident, Monitor, and ScheduledMaintenance 2025-10-30 11:59:07 +00:00
Nawaz Dhandala
709b9b8343 fix(link): add title to ComponentProps and forward it to the anchor element 2025-10-30 11:36:46 +00:00
Nawaz Dhandala
aa93252407 Merge branch 'master' into release 2025-10-29 20:38:27 +00:00
Simon Larsen
b4e7ffce43 Merge pull request #2062 from OneUptime/log-ui
Log UI
2025-10-29 20:37:16 +00:00
Nawaz Dhandala
1053d22d3f refactor(logs-viewer): add overflow-y-hidden to table container to prevent vertical overflow 2025-10-29 20:35:09 +00:00
Nawaz Dhandala
a37bdb303d refactor(logs-viewer): simplify empty-state copy, remove decorative '>' and tone down text colors 2025-10-29 20:33:31 +00:00
Nawaz Dhandala
860af1bbf3 refactor(logs-viewer): polish empty-state layout and styling
Replace bare empty message with a centered card-style panel:
- add full-height centered wrapper with slate background
- constrain width, add rounded border, padding and inner shadow
- update typography to monospace, uppercase tracking and emerald accent
- tweak spacing and muted text color for helper copy
2025-10-29 20:30:26 +00:00
Nawaz Dhandala
d5e72c1af2 refactor(logs-viewer): normalize formatting and tidy JSX
- Clean up multiline signatures and expressions for readability
- Standardize JSX tag formatting and expanded-content rendering (explicit return)
- Minor whitespace/indentation tweaks in LogsViewer, LogsTable, LogDetailsPanel, and Dashboard LogsViewer

No functional changes.
2025-10-29 20:29:07 +00:00
Nawaz Dhandala
6cc6fc4bf1 refactor(logs-viewer): polish LogDetailsPanel layout, surface cards, badges and trace/span links 2025-10-29 20:27:50 +00:00
Nawaz Dhandala
f890f24650 refactor(logs-viewer): use icon sort indicators and refine table styling
- replace text-based sort markers with Icon (Chevron/ArrowUpDown) and add helper funcs
- adjust table/header/tbody classes (dividers, background, text colors)
- tweak row hover/selected styles and message/service text colors for better contrast
- update small typography and trace/span color for consistency
2025-10-29 20:22:34 +00:00
Nawaz Dhandala
d077e55241 refactor(logs-viewer): add pagination & sorting API, move sort controls to table headers
- Add server/client-aware pagination & sorting to LogsViewer (props: totalCount, page, pageSize, onPageChange, onPageSizeChange, sortField, sortOrder, onSortChange)
- Implement client-side sort fallback, severity-weight sorting and stable time sorting
- Move sort UI into LogsTable column headers and remove autoscroll / toolbar sort buttons
- Introduce internal page/size/sort state, reset behavior on filter/apply, and improved displayedLogs handling
- Update page size defaults and options (DEFAULT_PAGE_SIZE=100, PAGE_SIZE_OPTIONS=[100,250,500,1000])
- Wire Dashboard viewer to new API (pass totalCount, page, pageSize, sort handlers) and adjust realtime fetch logic
- Reduce default page limit usages from 250 to 100
2025-10-29 20:07:59 +00:00
Nawaz Dhandala
a4b7c99b8a refactor(logs-viewer): replace gradients with solid backgrounds and enable log message wrapping 2025-10-29 19:18:06 +00:00
Nawaz Dhandala
671be425ae refactor(logs-viewer): render log details inline and unify panel variants
- Add renderExpandedContent prop to LogsTable and render LogDetailsPanel inline for expanded rows
- Introduce "embedded" variant in LogDetailsPanel and extract container/header/card classes for shared styling
- Update table and surrounding container styles (rounded, gradients, backdrop) and add a click-to-open hint banner
- Remove standalone selectedLog memo and the separate details pane UI; use selectedLogId to control inline expansion
2025-10-29 19:01:53 +00:00
Nawaz Dhandala
d2d7a51842 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-29 18:24:49 +00:00
Simon Larsen
1142a20d64 refactor(logs-viewer): tighten types and clean up formatting across components
- Add explicit type annotations for callbacks, helpers and theme (SeverityTheme)
- Strengthen severityTheme getSeverityTheme signature and consume typed return
- Remove unused catch variables and mark unused params where applicable
- Minor JSX/formatting tweaks and whitespace cleanup in table, pagination, details panel
- Small import/format fix in Execute.ts
2025-10-29 18:02:19 +00:00
Simon Larsen
8d0d7dc759 refactor(logs-viewer): modularize viewer into table, pagination and details panel
- Remove legacy LogItem and the old scroll-based monolithic layout.
- Add modular components under LogsViewer/components:
  - LogsTable (tabular list view, row selection, copy actions)
  - LogDetailsPanel (expanded details, copy buttons, trace/span links)
  - LogsViewerToolbar (autoscroll toggle, sort controls, summary)
  - LogsPagination (page controls & page-size selector)
  - LogsFilterCard (filters wrapper using FiltersForm)
  - SeverityBadge + severityTheme (centralized severity styling)
- Replace manual scroll alignment with page-based auto-scroll and safer pagination logic.
- Preserve getTraceRoute/getSpanRoute behavior and filter lifecycle (attributes loading, apply filters).
- Improve state handling (selection, pagination, page size, service map loading) and reduce component complexity.
2025-10-29 17:52:26 +00:00
Simon Larsen
cedf06ba42 chore(ci): schedule npm-audit-fix workflow and create PR via action
Switch workflow to run on a daily schedule (and via workflow_dispatch) and
replace direct git commit/push steps with peter-evans/create-pull-request to
open changes as a PR instead of pushing to the default branch.
2025-10-29 17:02:50 +00:00
Nawaz Dhandala
b347e18749 chore: update package-locks — add deps, bump versions & sync subdeps
- Update lockfiles for root, TestServer, Worker and Workflow
- Add new/top-level packages and types:
  - @simplewebauthn/server, archiver, botbuilder
  - @types/archiver
- Bump notable package versions:
  - axios, nodemailer, playwright, react-syntax-highlighter
- Sync a large set of transitive dependency upgrades:
  - @babel/* (code-frame, parser, template, types, helpers, helper-*), brace-expansion, braces, fill-range, micromatch, picocolors, cross-spawn, to-regex-range and others
- Remove/clean outdated lock entries (e.g. to-fast-properties removed where unnecessary)
- Add/fix license fields for several packages in the lockfiles
- General lockfile normalization to ensure consistent subdependency versions across packages

This keeps dependency trees aligned across monorepo packages and pulls in recent bug/security fixes from transitive upgrades.
2025-10-29 16:43:36 +00:00
Nawaz Dhandala
50e9a53547 chore(ci): don't mark whole run as failed when npm audit fix errors; only report the error 2025-10-29 16:26:40 +00:00
Nawaz Dhandala
d0de004498 docs(devpromps): fix typo in TerraformPrompt.md and add note to avoid editing generated provider code 2025-10-29 16:24:51 +00:00
Nawaz Dhandala
65c4998048 chore(isolated-vm): update package-lock.json — bump dependencies, add new & transitive packages 2025-10-29 16:12:16 +00:00
Nawaz Dhandala
5e7a3795c7 chore(ci): add npm-audit-fix workflow and helper script, expose audit-fix npm script 2025-10-29 13:25:31 +00:00
Nawaz Dhandala
cf83319a90 style(execute): normalize import formatting and reindent exec callback 2025-10-28 19:26:44 +00:00
Nawaz Dhandala
87dc9d88d0 chore(common): update package-lock.json with dependency bumps and transitive additions
Bump multiple deps (notably @babel/*, express, undici, brace-expansion, sha.js, call-bind)
Add transitive packages: call-bound, is-typed-array, to-buffer, typed-array-buffer
Include updated metadata (engines, funding) and adjust dev flags/pruned entries in lockfile
2025-10-28 19:24:48 +00:00
Nawaz Dhandala
444cf040a6 refactor(execute,code-repository): allow ExecOptions in Execute.executeCommand and use cwd instead of 'cd'
- Extend Execute.executeCommand to accept ExecOptions and forward them to child_process.exec
- Log stderr on error and debug-log stderr when present
- Update CodeRepository to pass cwd to Execute.executeCommand instead of prefixing commands with "cd"
2025-10-28 17:37:06 +00:00
Nawaz Dhandala
2754657a6f chore(probe): update package-lock with dependency version bumps and new packages
- Bump axios, playwright, playwright-core, react-syntax-highlighter, nodemailer
- Add @simplewebauthn/server, @types/archiver, archiver, botbuilder
- Update resolved versions/integrity in lockfile
2025-10-28 16:58:30 +00:00
Nawaz Dhandala
38ca6b1e9e style(code-repository): normalize logger debug formatting and wrap long expressions for readability 2025-10-28 16:52:54 +00:00
Nawaz Dhandala
f481ef4f5e refactor(code-repository,github): replace ad-hoc shell commands with structured git execution, sanitize paths, and improve logging
- Use CodeRepositoryUtil.runGitCommand / Execute.executeCommandFile instead of building shell command strings.
- Properly resolve and sanitize file paths when adding files to git; skip empty/invalid paths and add relative sanitized paths.
- Use resolvePathWithinRepo and path.relative for accurate file/argument resolution.
- Trim returned git commit hash.
- Improve debug messages for branch creation/checkout, user.name setting, adding remotes, and pushing branches.
- Encode credentials when constructing remote URL for push and use cwd for git operations.
2025-10-28 16:50:30 +00:00
Nawaz Dhandala
ad9adca473 docs(devpromps): add SecurityFix.md prompt to locate and fix security vulnerabilities 2025-10-28 16:04:47 +00:00
Nawaz Dhandala
819bd54a1f refactor(worker,server): remove unused import, add missing LIMIT_PER_PROJECT import, and normalize object literal formatting 2025-10-28 15:00:24 +00:00
Nawaz Dhandala
e212079b4a refactor(database,worker): remove batchSize option and default to LIMIT_MAX
- Remove batchSize field from FindAllBy type.
- Update DatabaseService.findAllBy to stop reading/validating batchSize and use LIMIT_MAX as the batch size.
- Remove per-job batch size constants and batchSize parameters from multiple worker cron jobs (AlertOwners, Incident*, ScheduledMaintenance*, StatusPage*, Announcement, OnCall, Workflow, UserOnCallLog, ServerMonitor, PaymentProvider, etc.).
- Simplify call sites to rely on findAllBy's default batching behavior.
2025-10-28 14:56:38 +00:00
Nawaz Dhandala
bb09dafbcc refactor(database,worker): add paginated findAllBy and migrate jobs to batch fetching
- Add FindAllBy type and implement DatabaseService.findAllBy with batchSize/limit/skip support.
- Add normalizePositiveNumber helper used by findAllBy.
- Add ProjectService.getAllActiveProjects convenience wrapper that uses findAllBy.
- Replace many worker cron jobs' findBy calls with findAllBy, introduce per-job batch size constants, remove LIMIT_MAX imports, and pass batchSize/skip instead of one large limit.
- Convert long-running deletes/hard-deletes to paginated loops (repeat until no more records deleted).
- Adjust various query usages to use LIMIT_PER_PROJECT where appropriate as batchSize.
2025-10-28 14:32:45 +00:00
Nawaz Dhandala
60c472cc09 refactor(monitor-resource): replace Metric/MonitorLog models with JSON rows and add builders
Introduce buildMonitorMetricAttributes and buildMonitorMetricRow helpers to centralize attribute/timestamp logic. Replace creation of Metric and MonitorLog model instances with JSON row construction and use MetricService.insertJsonRows / MonitorLogService.insertJsonRows. Remove setAttributeKeys/Metric import usage and simplify metric aggregation code paths.
2025-10-28 12:33:12 +00:00
Nawaz Dhandala
0e272f0f31 refactor(fluent-ingest): replace Log model with JSON rows and add batching
- Remove dependency on Log model; build log JSON rows inline (IDs, ISO timestamps, unix nano)
- Buffer logs and flush in batches using new flushLogBuffer helper and LogService.insertJsonRows
- Add FLUENT_INGEST_LOG_FLUSH_BATCH_SIZE constant and simplify processing loop
2025-10-28 12:20:43 +00:00
Nawaz Dhandala
f5de74611d refactor(code-repository): centralize git execution with runGitCommand and improve clone folder detection
Replace ad-hoc shell command construction/Execute.executeCommand calls with a new runGitCommand wrapper that uses Execute.executeCommandFile. Update various methods (getCurrentCommitHash, addAllChangedFilesToGit, setAuthorIdentity, discardAllChangesOnCurrentBranch, pullChanges, createOrCheckoutBranch, discardChanges, etc.) to use the helper. Also improve cloneRepository to derive the cloned folder name from the repo URL and throw a clear error if it can't be determined.
2025-10-28 12:14:00 +00:00
Nawaz Dhandala
03d157b850 refactor(identity,code-repository): allow SCIM group updates by removing isTeamEditable guard; type readDirectory entries as fs.Dirent 2025-10-27 20:50:27 +00:00
Nawaz Dhandala
da21cfc1ff refactor(code-repository,local-file): normalize arrow return style and reformat sanitizeFilePath/readDirectory signatures 2025-10-27 17:53:18 +00:00
Nawaz Dhandala
2781bf0583 refactor(execute,code-repository): add executeCommandFile (execFile wrapper) and use it in commitChanges 2025-10-27 17:52:00 +00:00
Nawaz Dhandala
69b16c1c85 refactor(code-repository): use LocalFile.read for getFileContent and add resolvePathWithinRepo to sanitize/validate paths 2025-10-27 17:47:49 +00:00
Nawaz Dhandala
727f009d79 refactor(code-repository): replace shell ls/file usage with LocalFile.readDirectory and Dirent checks; add readDirectory helper 2025-10-27 17:44:00 +00:00
Nawaz Dhandala
65d916f349 refactor(code-repository): replace shell rm -rf with LocalFile.deleteDirectory and add LocalFile.deleteDirectory helper 2025-10-27 17:40:02 +00:00
Nawaz Dhandala
4373c7b49c refactor(telemetry): collapse multi-line if into single-line in TelemetryUsageBillingService 2025-10-27 16:29:41 +00:00
Nawaz Dhandala
34737fbba4 feat(telemetry): account for Exceptions in usage billing and add avg exception row size
- Update TelemetryUsageBilling description to include Exceptions.
- Add AverageExceptionRowSizeInBytes env/config (env example, docker-compose, Helm values & schema).
- Use ExceptionInstanceService in TelemetryUsageBillingService to include exception row counts when estimating bytes for Traces.
- Add helper to read average exception row size and adjust billing calculations.
2025-10-27 16:26:46 +00:00
Nawaz Dhandala
90fcfd1c7e Merge remote-tracking branch 'origin/snyk-upgrade-89594c7cd11449f44c7d156474899af6' 2025-10-27 15:49:24 +00:00
Simon Larsen
b55320f02c Merge pull request #2059 from OneUptime/otel-writer
Efficient Otel Writer
2025-10-27 15:45:51 +00:00
Nawaz Dhandala
34dc078197 fix(analytics): set wait_for_async_insert=0 for ClickHouse async inserts and tidy formatting 2025-10-27 15:45:20 +00:00
Nawaz Dhandala
2eacc90714 refactor(opentelemetry): format timestamps as ClickHouse UTC datetimes
- add OneUptimeDate.toClickhouseDateTime to produce UTC "YYYY-MM-DD HH:mm:ss"
- use ClickHouse-formatted timestamps for createdAt/updatedAt and time fields in OtelLogsIngestService, OtelMetricsIngestService and OtelTracesIngestService
- extend metric timestamp parsing to include db/date (and propagate db for DB storage)
- switch intermediate handling to Date objects to avoid extra ISO-string conversions
2025-10-27 15:41:13 +00:00
Nawaz Dhandala
9d93d59f91 fix(analytics): set wait_for_async_insert=1 and log ClickHouse insert result 2025-10-27 15:19:58 +00:00
Nawaz Dhandala
d84039e621 refactor(analytics): make ClickHouse client nullable and centralize retrieval
Change databaseClient to ClickhouseClient | null and add getDatabaseClient() to
lazily refresh/validate the client. Update insert/execute/executeQuery and
useDefaultDatabase to use the centralized getter and remove unsafe casts and
duplicated null checks.
2025-10-27 15:13:10 +00:00
Nawaz Dhandala
4eb46cf8a0 docs(clickhouse): add SQL to calculate average uncompressed row size per table and close code fence 2025-10-27 13:46:24 +00:00
Nawaz Dhandala
1ef27b7f52 fix(settings): clarify usage history no-items message to mention end-of-day data availability 2025-10-27 13:22:28 +00:00
Nawaz Dhandala
412bd370df refactor(telemetry): add explicit types for attribute value variables in TelemetryUtil 2025-10-27 13:20:33 +00:00
Nawaz Dhandala
1131b80a52 style(otel-ingest): normalize .catch callback formatting in span exception handling 2025-10-27 13:16:16 +00:00
Nawaz Dhandala
8b55f5c348 refactor(otel-ingest): decouple exception handling from analytics model
- Remove direct usage of ExceptionInstance model in OtelTracesIngestService.
- Build and push exceptions as ExceptionEventPayload objects, compute fingerprint from payload.
- Update buildExceptionRow to accept ExceptionEventPayload and map fields accordingly.
- Change ExceptionUtil API: introduce ExceptionFingerprintInput and TelemetryExceptionPayload types; update getFingerprint and saveOrUpdateTelemetryException signatures.
2025-10-27 13:14:07 +00:00
Nawaz Dhandala
159e5c4023 refactor(otel-ingest): switch telemetry ingestion to ClickHouse bulk writer
Replace model-based createMany calls with ClickhouseBulkWriter.insert for logs, metrics, spans and exceptions. Build plain JSON rows (with createdAt/updatedAt, timestamps, attributes, attributeKeys, trace/span ids, severity, etc.) and change buffers to JSONObject arrays. Add helper builders and utilities (buildMetricRow, buildSpanRow, buildExceptionRow, safeParseUnixNano, toNumberOrNull, toBoolean, convertBase64ToHexSafe, calculateDurationNano) to improve timestamp/attribute parsing and robustness. Add new ClickhouseBulkWriter utility.
2025-10-27 13:05:49 +00:00
Nawaz Dhandala
4970538d43 docs: correct example pagination request method to POST 2025-10-27 12:39:22 +00:00
Nawaz Dhandala
d7ca021d52 refactor: improve TelemetryUtil attribute parsing, typing and null handling 2025-10-27 12:17:31 +00:00
Simon Larsen
27eff7f415 Merge pull request #2057 from OneUptime/snyk-upgrade-ef9e383973f8732d2d959dd964599c61
[Snyk] Upgrade axios from 1.12.0 to 1.12.2
2025-10-27 10:32:46 +00:00
Simon Larsen
50543ec7bf Merge pull request #2058 from OneUptime/snyk-upgrade-7e0a372e41cd67fc4ed46876443edaa8
[Snyk] Upgrade react-router-dom from 6.23.1 to 6.30.1
2025-10-27 10:32:39 +00:00
snyk-bot
94c39408ed fix: upgrade react-router-dom from 6.23.1 to 6.30.1
Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.30.1.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/11bb5528-45f9-473c-a635-dc097fd03b3c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-27 09:53:27 +00:00
snyk-bot
7219e1850f fix: upgrade axios from 1.12.0 to 1.12.2
Snyk has created this PR to upgrade axios from 1.12.0 to 1.12.2.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/49c81d9c-12c2-4e8e-b9e8-72f98b1b595c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-27 09:51:39 +00:00
Simon Larsen
3180ed7149 Merge pull request #2055 from OneUptime/snyk-upgrade-9e6de76051d1dee73345ae37a0e51d09
[Snyk] Upgrade react-router-dom from 6.23.1 to 6.30.1
2025-10-26 17:24:46 +00:00
Simon Larsen
79f32b80c8 Merge pull request #2054 from OneUptime/snyk-upgrade-bad1e870c8252b45aa78fe78e19570b0
[Snyk] Upgrade globals from 15.6.0 to 15.15.0
2025-10-26 17:24:27 +00:00
Simon Larsen
cebfde6bf2 Merge pull request #2052 from OneUptime/snyk-upgrade-7a970ceafec3776b6998e6c88441b566
[Snyk] Upgrade typeorm from 0.3.20 to 0.3.27
2025-10-26 17:24:05 +00:00
Simon Larsen
ca644d9dc7 Merge pull request #2051 from OneUptime/snyk-upgrade-6a98a9360c422231b9e06b3e3624ea4f
[Snyk] Upgrade @readme/openapi-parser from 4.1.0 to 4.1.2
2025-10-26 17:23:59 +00:00
snyk-bot
ca4d9cb176 fix: upgrade react-router-dom from 6.23.1 to 6.30.1
Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.30.1.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/8ca4ee75-8bc5-43a1-a3bc-244ceebf1437?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-26 10:52:13 +00:00
snyk-bot
56204e02a9 fix: upgrade globals from 15.6.0 to 15.15.0
Snyk has created this PR to upgrade globals from 15.6.0 to 15.15.0.

See this package in npm:
globals

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/c3622982-05c8-495c-809c-20f301c75f92?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-25 11:38:53 +00:00
snyk-bot
c8e8a6d687 fix: upgrade jest from 30.0.0 to 30.2.0
Snyk has created this PR to upgrade jest from 30.0.0 to 30.2.0.

See this package in npm:
jest

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/c3622982-05c8-495c-809c-20f301c75f92?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-25 11:38:49 +00:00
snyk-bot
5bd2204eee fix: upgrade typeorm from 0.3.20 to 0.3.27
Snyk has created this PR to upgrade typeorm from 0.3.20 to 0.3.27.

See this package in npm:
typeorm

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/c3622982-05c8-495c-809c-20f301c75f92?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-25 11:38:44 +00:00
snyk-bot
72a31ed268 fix: upgrade @readme/openapi-parser from 4.1.0 to 4.1.2
Snyk has created this PR to upgrade @readme/openapi-parser from 4.1.0 to 4.1.2.

See this package in npm:
@readme/openapi-parser

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/f8557d79-3b3d-4201-8e2b-598120aedb5c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-25 11:34:15 +00:00
Nawaz Dhandala
ee188dd050 chore(values.schema): add noProxy option to proxy configuration 2025-10-24 17:48:29 +01:00
Nawaz Dhandala
3807aad63b Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-24 17:40:33 +01:00
Nawaz Dhandala
055ec956fd build(esbuild-config): add refractor compatibility plugin to resolve refractor imports
Add createRefractorCompatibilityPlugin that maps imports from refractor/lib and refractor/lang
to the local refractor package (searching candidate node_modules paths), and include it in the
plugins list so esbuild can correctly resolve refractor modules.
2025-10-24 17:40:31 +01:00
Simon Larsen
e7767e59d1 Merge pull request #2048 from OneUptime/probe-no-prpoxy
Probe no prpoxy
2025-10-24 17:24:47 +01:00
Nawaz Dhandala
f643e907b4 refactor(proxy-config): normalize & resolve ports for NO_PROXY matching; add protocol resolution helpers 2025-10-24 17:24:22 +01:00
Nawaz Dhandala
2eec57befd refactor(proxy-config): robust hostname/port extraction, normalize ports, and tidy formatting
- Improve extractHostnameAndPort and splitHostAndPort to handle IPv6 brackets, single-colon ports, and trimmed port values; return explicit result objects.
- Normalize host/port parsing logic and ensure port values are trimmed before use.
- Consolidate import formatting and split long method signatures for readability.
- Minor formatting/whitespace cleanups in Probe Config, Alive, Register, Monitor, and NO_PROXY parsing.
2025-10-24 15:12:12 +01:00
Nawaz Dhandala
dd653f8deb feat(proxy): add NO_PROXY support and per-request proxy bypass based on target URL
- import NO_PROXY and log its entries during configuration
- allow callers to pass a target URL to getHttpProxyAgent/getHttpsProxyAgent/getRequestProxyAgents
- implement shouldBypassProxy with URL/host:port extraction and pattern matching (including wildcard, subdomains, IPv6, and scheme/port-aware patterns)
- return no agents when a target matches NO_PROXY or when proxy is not configured
2025-10-24 15:04:05 +01:00
Nawaz Dhandala
f403c6a9e9 feat(proxy): add NO_PROXY support and use request URL for proxy agent selection
- Parse NO_PROXY / no_proxy in Probe Config into a trimmed list
- Wire NO_PROXY into UI docs, Helm chart values, and probe Docker/compose examples
- Add NO_PROXY env var to Helm probe template when provided
- Pass target URL to ProxyConfig.getRequestProxyAgents / getHttpProxyAgent / getHttpsProxyAgent so proxy selection is per-request
- Update probe calls (Alive, Metrics, FetchList, FetchMonitorTest, Register, Monitor ingest/reporting, Api/Website/Ssl monitors) to use local URL variables and supply them to proxy helpers
- Minor refactors to avoid inline URL construction where reused
2025-10-24 15:02:36 +01:00
Nawaz Dhandala
35f9b7f5c4 refactor(log-item): move collapse toggle to header container and remove redundant handler 2025-10-24 13:16:11 +01:00
Nawaz Dhandala
3c487ff9b9 refactor(log-item): use monospaced font for collapsed row and message previews; make header toggleable 2025-10-24 13:11:37 +01:00
Nawaz Dhandala
41fca346b9 refactor(table-row): extract columnContent and consolidate column rendering
- Move per-column value/element logic into a single columnContent variable
- Add contentWrapperClassName and actionsContainerClassName to unify wrappers
- Remove duplicated JSX branches for desktop row cells and simplify action rendering
2025-10-24 13:00:24 +01:00
Nawaz Dhandala
91b54ced67 refactor(exceptions): normalize icon sizes in TelemetryExceptionElement by adding min-h-6 and min-w-6 to icon wrappers 2025-10-24 12:59:49 +01:00
Nawaz Dhandala
ebdd97b8e9 refactor(exceptions): normalize icon sizes and simplify className usage in TelemetryExceptionElement
- Remove redundant h-6 w-6 classes from Icon elements and rely on wrapper sizing.
- Simplify container className interpolation (remove fallback empty string).
2025-10-24 12:56:13 +01:00
Nawaz Dhandala
8eb1eac629 refactor(table-row): inline column content rendering and remove temporary wrapper variables 2025-10-24 12:53:15 +01:00
Nawaz Dhandala
a075b3c4dd refactor(table-row, exceptions): dedupe column rendering, wrap content, and normalize icon sizes
- Extract columnContent and content/action wrapper class names in TableRow to remove duplicated conditional rendering and wrap cell content with the configured className.
- Replace inline action container with actionsContainerClassName for consistent alignment.
- Add explicit h-6 w-6 classes to exception icons for consistent sizing.
2025-10-24 12:50:59 +01:00
Nawaz Dhandala
738f901a51 refactor(vm-runner, logs-viewer, server-monitor, embedded-status): simplify script generation, add explicit typings, and tidy UI text 2025-10-24 12:18:25 +01:00
Nawaz Dhandala
683a8f5a58 chore(eslint): enable no-constant-binary-expression rule (set to error) 2025-10-24 12:11:30 +01:00
Nawaz Dhandala
160eba1ea4 refactor(exceptions): expose className prop on TelemetryExceptionElement and pass max-w-3xl from ExceptionsTable 2025-10-24 12:09:49 +01:00
Nawaz Dhandala
89b65d1e02 style(logs-viewer): inline ternary expressions in Icon className strings 2025-10-24 12:06:38 +01:00
Nawaz Dhandala
caf709a38a chore(probe): bump Node base image to public.ecr.aws/docker/library/node:24.9 (from 23.8) 2025-10-24 12:01:50 +01:00
Simon Larsen
d57433e4a0 Merge pull request #2045 from OneUptime/telemetry-billing
Telemetry billing
2025-10-24 11:08:18 +01:00
Simon Larsen
5e2aa4e622 Merge pull request #2044 from OneUptime/log-fix
feat(logs-viewer): add toggleable log ordering and update scroll-to-l…
2025-10-24 10:13:40 +01:00
Nawaz Dhandala
6cb51dd54b refactor(logs-viewer): replace sort toggle with segmented "Newest first"/"Oldest first" buttons and extract applySortDirection
Extract applySortDirection to centralize sort toggle + scroll alignment logic and replace the single ArrowUpDown button with two styled buttons (BarsArrowDown / BarsArrowUp) to improve UX and clarity.
2025-10-24 10:11:45 +01:00
Nawaz Dhandala
53ea843bdc Merge branch 'master' into log-fix 2025-10-24 09:52:13 +01:00
Nawaz Dhandala
71eeaf7ecd chore(dashboard): add archiver/@simplewebauthn/server and bump several deps
- add @simplewebauthn/server, archiver, @types/archiver, botbuilder
- bump nodemailer -> ^7.0.7, playwright -> ^1.55.1, react-syntax-highlighter -> ^16.0.0
2025-10-24 09:51:54 +01:00
Simon Larsen
1d0168fcc6 Merge pull request #2043 from OneUptime/embedded-sttaus
Embedded sttaus
2025-10-24 09:49:47 +01:00
Nawaz Dhandala
99c3d440c5 refactor(status-page): remove inline token exposure warning from Embedded Status Badge card 2025-10-24 09:39:12 +01:00
Nawaz Dhandala
5959ce728f refactor(status-page): wrap Embedded Status CardModelDetail in container and tidy props
Wrap the CardModelDetail for the embedded status badge in a surrounding <div> to group it with the other cards, and reorganize/tidy card/form properties (move description to cardProps, clean up formField ordering and types). Also minor JSX/formatting cleanup.
2025-10-24 09:38:47 +01:00
Nawaz Dhandala
0cb9e382a6 refactor(status-page): use provisioned domain for badge docs, replace Alert with inline warning, and tidy embedded badge UI
- Fetch latest SSL-provisioned StatusPageDomain and build a status page URL to use in the badge documentation/markdown embeds (falls back to STATUS_PAGE_URL).
- Add state for statusPageUrl and effect to resolve domain via ModelAPI (Query/Sort).
- Replace Alert component with inline informational paragraph and a highlighted yellow warning box.
- Minor cleanup: remove an obsolete comment, adjust Card layout/spacing and documentation markdown link.
2025-10-24 09:37:04 +01:00
Nawaz Dhandala
0c6d561b7c refactor(status-page): remove redundant "Badge Usage" header and drop h-full from token/preview cards
Remove the "## Badge Usage" heading from the documentation markdown and remove the
className="h-full" from the Security Token and Badge Preview Card components to
simplify and stabilize the embedded badge layout.
2025-10-24 09:21:05 +01:00
Nawaz Dhandala
4291a76dd4 refactor(exceptions/status-page): improve exception message wrapping and simplify embedded badge layout
- Add contentClassName to the "Exception Message" column to constrain width and allow proper wrapping.
- Remove the surrounding grid wrapper and the lg:col-span-2 on the Badge Documentation card to simplify the embedded badge layout.
2025-10-24 09:14:53 +01:00
Nawaz Dhandala
05b1f0ea82 refactor(status-page): use Alert component for token regeneration warning in embedded badge card 2025-10-24 09:12:41 +01:00
Nawaz Dhandala
b943505b1d refactor(status-page): include embeddedOverallStatusToken in modelDetailProps 2025-10-24 09:08:31 +01:00
Simon Larsen
1ed236eb91 Merge pull request #2046 from OneUptime/snyk-upgrade-6c5f58f1b138b8c82cfd6bdcdf7dbcce
[Snyk] Upgrade react-router-dom from 6.23.1 to 6.30.1
2025-10-24 09:02:22 +01:00
snyk-bot
44795182c9 fix: upgrade react-router-dom from 6.23.1 to 6.30.1
Snyk has created this PR to upgrade react-router-dom from 6.23.1 to 6.30.1.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/5dd2ef9c-1270-4729-aff4-e407805f7a9c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-10-24 04:01:13 +00:00
Simon Larsen
50267534e0 Merge pull request #2003 from OneUptime/snyk-fix-2b1cd110cd50d4d7d2d3aab8e21c4982
[Snyk] Security upgrade axios from 1.7.2 to 1.12.0
2025-10-23 21:24:39 +01:00
Simon Larsen
f60836a4dc Merge pull request #2027 from OneUptime/snyk-fix-12d4523821beb1a61df8fdd2378e8e5f
[Snyk] Security upgrade nodemailer from 6.9.16 to 7.0.7
2025-10-23 21:24:33 +01:00
Simon Larsen
303294bb5e Merge pull request #2038 from OneUptime/snyk-fix-bb0bdd9905b96cdaeceb0ee81316a34f
[Snyk] Security upgrade playwright from 1.50.0 to 1.55.1
2025-10-23 21:21:36 +01:00
Simon Larsen
1212a8e4be Merge pull request #2031 from OneUptime/snyk-fix-e2f14fffff98beb03eb482adc84b328d
[Snyk] Security upgrade nginx from 1.28.0-alpine to 1.29.2-alpine
2025-10-23 21:21:29 +01:00
Simon Larsen
25c626d2d4 Merge pull request #2030 from OneUptime/snyk-fix-311183f6327ea1b45e86908c4fe82d3a
[Snyk] Security upgrade mailparser from 3.7.1 to 3.7.5
2025-10-23 21:21:20 +01:00
Simon Larsen
ff6f9c89fa Merge pull request #2040 from OneUptime/snyk-fix-66c920d77215ccde82d8cbc57644a387
[Snyk] Fix for 2 vulnerabilities
2025-10-23 21:20:49 +01:00
Nawaz Dhandala
7f6e905c74 refactor(markdown-viewer): reduce padding for pre and syntax highlighter code blocks 2025-10-23 21:04:33 +01:00
Nawaz Dhandala
2cc64838aa refactor(status-page): keep token in state after regeneration, move regen button to card header, and tweak layout/preview styles
- Remove unused default Button import
- Replace window.location.reload() with setToken(newToken) so UI updates without a full page reload
- Move "Regenerate Token" into Card.buttons with loading/disabled states
- Rework card layout into responsive grid, add bodyClassName, and adjust preview styling (dashed container, max height for badge)
2025-10-23 20:49:07 +01:00
Nawaz Dhandala
3de5c8da8c refactor(status-page): consolidate token rotation UI, use useMemo for modelId, fix side menu icon
- Wrap modelId retrieval in useMemo for stable value
- Move "Regenerate Token" button and helper text into the Security Token card and remove the separate "Token Rotation" card
- Update SideMenu icon for "HTML, CSS & JavaScript" to IconProp.Circle
2025-10-23 20:41:53 +01:00
Nawaz Dhandala
7403ffa053 refactor(status-page): simplify embedded badge state and docs UI
- Remove statusPage state and store embeddedOverallStatusToken and enableEmbeddedOverallStatus
  in separate token and isEmbeddedStatusEnabled states.
- Set token and enabled flag directly in onItemLoaded.
- Simplify "Badge Documentation" card to render MarkdownViewer only (remove raw source/preview split)
- Tweak card description copy.
2025-10-23 20:37:03 +01:00
Nawaz Dhandala
fa473474a2 feat(status-page): improve embedded badge UI, use secure token generation, add preview & docs
- Replace non-cryptographic token generation with ObjectID.generate()
- Add statusPage state, token/isEnabled checks and computed badge URLs
- Split UI into Security Token, Token Rotation, Badge Preview and Badge Documentation cards
- Use HiddenText for copyable token and MarkdownViewer for rendered docs
- Wire CardModelDetail onItemLoaded to populate statusPage for live preview
2025-10-23 20:33:45 +01:00
Nawaz Dhandala
20cbcf9a74 refactor(status-page): remove ModelPage wrapper and routing/side menu from EmbeddedStatus
- Return Fragment directly instead of wrapping with ModelPage
- Remove breadcrumb links and sideMenu usage
- Remove unused imports: PageMap, RouteMap, RouteUtil, Route, SideMenu, ModelPage
2025-10-23 20:12:17 +01:00
Nawaz Dhandala
e6fd4b8304 refactor(types): add explicit typings for env parser and telemetry billing
Add a full function signature for parsePositiveNumberFromEnv and strengthen typing in TelemetryUsageBillingService: import TelemetryServiceModel, type telemetryServices as Array<TelemetryServiceModel>, and annotate count results as PositiveNumber for traces, logs and metrics to improve type-safety and readability.
2025-10-23 20:02:57 +01:00
Nawaz Dhandala
4a3a743dcd chore(format): normalize multiline argument formatting in EnvironmentConfig and TelemetryUsageBillingService 2025-10-23 19:53:09 +01:00
Nawaz Dhandala
6f1e2234d3 feat(telemetry): stage daily usage and centralize telemetry billing
- Add stageTelemetryUsageForProject to TelemetryUsageBillingService:
  count traces/logs/metrics per telemetry service via AnalyticsQueryHelper,
  estimate bytes using average row size env vars, convert to GB and create/update
  daily usage records (uses day + usageDate for deduplication).
- Update updateUsageBilling to accept usageDate, set createdAt to usageDate and
  query by day instead of createdAt ranges.
- Add helper getAverageRowSizeForProduct and import LIMIT_INFINITY,
  DEFAULT_RETENTION_IN_DAYS, TelemetryServiceService, Span/Log/Metric services,
  DiskSize, AnalyticsQueryHelper and logger.
- Call stageTelemetryUsageForProject from TelemetryMeteredPlan before reporting
  to the billing provider.
- Remove direct billing invocation from FluentIngest worker to decouple ingestion
  from billing staging.
2025-10-23 19:51:11 +01:00
Nawaz Dhandala
07189b4567 feat(env): add average telemetry row size settings and expose as env vars
- add billing.telemetry.{averageSpanRowSizeInBytes, averageLogRowSizeInBytes, averageMetricRowSizeInBytes} to values.yaml with defaults
- add schema validation for these fields in values.schema.json (integer, minimum 1)
- expose AVERAGE_SPAN_ROW_SIZE_IN_BYTES, AVERAGE_LOG_ROW_SIZE_IN_BYTES, AVERAGE_METRIC_ROW_SIZE_IN_BYTES in templates/_helpers.tpl
2025-10-23 19:49:59 +01:00
Nawaz Dhandala
b1bc02cec4 feat(env): add average telemetry row size env vars with validation
- introduce parsePositiveNumberFromEnv helper in EnvironmentConfig.ts
- add AverageSpan/Log/MetricRowSizeInBytes exports with positive-number parsing and defaults
- document AVERAGE_* vars in config.example.env and expose them in docker-compose.base.yml
2025-10-23 19:49:24 +01:00
Simon Larsen
6f7795aa31 feat(logs-viewer): add toggleable log ordering and update scroll-to-latest behavior
- add isDescending state and useMemo displayLogs to reverse logs when needed
- rename showScrollToBottom -> showScrollToLatest and replace scrollToBottom with scrollToLatest
- convert handleScroll to useCallback and make it order-aware (top vs bottom)
- update scroll button icon/title and ensure scroll position recalculates after toggling order
- render and count displayLogs (use reversed list) and update empty state checks
- minor comment update in Dashboard LogsViewer
2025-10-23 19:24:02 +01:00
Nawaz Dhandala
50ee87c86f chore(mobile): remove MobileApp configuration files
Remove obsolete MobileApp project configuration and dependency files to clean up the repository:
- MobileApp/package.json
- MobileApp/tailwind.config.js
- MobileApp/tsconfig.json

These files contained Expo / React Native specific settings (dependencies, Nativewind/Tailwind config, and TS config) for a mobile app that is no longer maintained in this repo. Removing them reduces clutter and prevents confusion about supported platforms.
2025-10-23 17:21:20 +01:00
Simon Larsen
a75c6b6a43 feat(status-page): add Embedded Status page with token regeneration and wire up route/menu; remove legacy Embedded component 2025-10-23 16:53:58 +01:00
Simon Larsen
f6168c969e feat(status-page): add embedded status badge UI and token regeneration
- Add advanced options UI for embedded overall status badge: enable toggle, security token display, badge preview, and HTML/Markdown embed examples.
- Implement regenerate token flow with confirmation modal and regenerateToken function that creates a new token and updates the StatusPage via ModelAPI.
- Add local state hooks for modal/loading and construct badge URL with token placeholder.
- Rename/export component from StatusPageDelete to StatusPageAdvancedOptions.
2025-10-23 16:26:45 +01:00
Simon Larsen
2b0f9f2e7a feat(status-page): add embedded overall status badge toggle and token with migration
- Add enableEmbeddedOverallStatus boolean column and embeddedOverallStatusToken (with index) to StatusPage model
- Include billing and column access controls for new fields
- Add corresponding TypeORM migration and register it in migrations index
2025-10-23 16:20:18 +01:00
Nawaz Dhandala
14377c68fe Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2025-10-23 15:45:59 +01:00
Nawaz Dhandala
9176fa2c9b style(forms): add className override to FieldLabel and apply to Dictionary labels 2025-10-23 15:44:26 +01:00
Nawaz Dhandala
4f29fef5f6 feat(model-filter): add isAdvancedFilter and propagate to table
Add isAdvancedFilter to Filter interface, pass it into the ClassicFilter mapping
in BaseModelTable, and include props.filters in the showFilterModal effect deps
so filters refresh when changed. Mark the TraceTable "Attributes" filter as an
advanced filter.
2025-10-23 13:46:06 +01:00
Simon Larsen
f28c7695ab Merge pull request #2041 from OneUptime/master
Release
2025-10-22 18:16:10 +01:00
Nawaz Dhandala
bc234deb0e style(analytics): simplify empty projections to [] in Log/Metric/Span and remove stray blank line in TableManegement 2025-10-22 18:08:18 +01:00
Nawaz Dhandala
546b4a4fb3 refactor(analytics): expose AnalyticsTableManagement helpers and remove projection/materialized view creation from createTables
- Make utility methods public: doesProjectionExist, materializeProjection, escapeForQuery, escapeIdentifier, doesMaterializedViewExist, createMaterializedView
- Remove inline projection and materialized view creation logic from createTables (createTables now only ensures tables exist)
- Remove unused Projection import
2025-10-22 18:07:45 +01:00
Nawaz Dhandala
1300c4e667 feat(analytics): add materialized view support to analytics models and table management
- add MaterializedView type
- wire materializedViews into AnalyticsBaseModel (constructor param, property, getter/setter)
- implement materialized view processing in AnalyticsTableManagement (validation, existence check, creation)
2025-10-22 18:04:51 +01:00
Nawaz Dhandala
988d5d327c style(analytics): remove attribute projection definitions from Log, Metric and Span models 2025-10-22 17:52:44 +01:00
Nawaz Dhandala
f2510a7b89 style(exceptions): format message div across multiple lines for improved readability 2025-10-22 15:05:54 +01:00
Nawaz Dhandala
a2f16ca0eb style(exceptions): remove container truncate and add break-words to message text to allow long words to wrap 2025-10-22 15:04:44 +01:00
Nawaz Dhandala
1448288395 style(autocompleteTextInput,dictionary): normalize type annotations and tidy JSX formatting 2025-10-22 14:58:24 +01:00
Nawaz Dhandala
734481df6c style(dictionary): replace equals Icon with '=' text, adjust item row spacing/alignment, and remove unused Icon import 2025-10-22 14:57:00 +01:00
Nawaz Dhandala
43c534b76a feat(dictionary): add FieldLabelElement labels for Key, Type and Value fields 2025-10-22 14:44:31 +01:00
Nawaz Dhandala
1b593403b6 refactor(dictionary): remove autoConvertValueTypes and implicit conversions
- Remove NumberUtil/BooleanUtil and the autoConvertValueTypes prop from Dictionary
- Stop automatic string->number/boolean coercion; preserve original values (normalize undefined/null to "")
- Use getDefaultValueForType when adding or changing a value's type
- Update JSONFilter and FormField to import ValueType and pass explicit valueTypes
2025-10-22 14:40:59 +01:00
Nawaz Dhandala
54e60cc380 feat(dictionary): replace key input/dropdown with AutocompleteTextInput and add component 2025-10-22 14:24:49 +01:00
Nawaz Dhandala
28d9879dbd style(logs,traces): add explicit typings and clean up callback formatting
- LogItem: annotate onClick/onAuxClick handlers with React.MouseEvent<HTMLDivElement>
- LogsViewer: add OptionalTraceRouteProps/OptionalSpanRouteProps types and type annotate trace/span route props passed to LogItem
- TraceExplorer: introduce/clarify function type aliases (PromiseVoidFunction, FetchSpansFunction) and annotate callbacks (fetchTelemetryServices, fetchSpans, fetchItems, handleShowNextSpans, handleShowAllSpans); reflow long JSX for readability
2025-10-22 14:08:18 +01:00
Nawaz Dhandala
22c417ac92 feat(traces): show compact loading pill for span load actions in TraceExplorer
Replace disabled "Show next" / "Show all" buttons with a single inline loading indicator while additional spans are being fetched, and restore the action buttons when not loading to improve loading UX and reduce visual clutter.
2025-10-22 13:39:42 +01:00
Nawaz Dhandala
401926c792 feat(traces): add paginated/batched span loading and load-more controls in TraceExplorer
Introduce INITIAL_SPAN_FETCH_SIZE, SPAN_PAGE_SIZE and MAX_SPAN_FETCH_BATCH and refactor span loading to support pagination and batching. Change fetchSpans to accept {limit, skip, mode} and return fetched count (replace | append). Remove spanLimitRef and add handleShowNextSpans / handleShowAllSpans to incrementally load spans (with batch loop for "show all"). Update UI to show a descriptive banner with "Show next" and "Show all remaining" buttons and improve loading/error state handling and metrics messaging.
2025-10-22 13:35:35 +01:00
Nawaz Dhandala
90bc4de84a feat(traces): incremental span loading and improved loading/error UX in TraceExplorer
- split telemetry & span fetching (fetchTelemetryServices, fetchSpans(limit, options))
- add totalSpanCount, spanLimitRef and isLoadingMoreSpans to support partial loading
- add handleShowAllSpans and UI banners/buttons to "Show N more spans" and indicate "Showing all N spans"
- show PageLoader only for initial empty-load; surface non-blocking errors inline when spans exist and keep blocking error when no spans
- show loaded/total counts in Spans metric
- reset span-related state (limit, spans, selection, total) when traceId changes
2025-10-22 13:17:16 +01:00
Nawaz Dhandala
93fd2a9ed7 style(date,gantt): reflow multiline returns in OneUptimeDate and format Row isHighlighted prop for readability 2025-10-22 12:27:31 +01:00
Nawaz Dhandala
231451e359 feat(date,span): add humanized duration formatter and use it in SpanUtil 2025-10-22 12:26:22 +01:00
snyk-bot
3073891a38 fix: Common/package.json & Common/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PLAYWRIGHTCORE-13553173
- https://snyk.io/vuln/SNYK-JS-PRISMJS-9055448
2025-10-22 11:16:45 +00:00
Nawaz Dhandala
2b62d31e8b fix(blog): set default pageSize to 25 for blog and tag listing endpoints 2025-10-22 12:04:21 +01:00
Nawaz Dhandala
bc43e6e6ea feat(traces): make card title/description reflect active tab (All / Root Spans) 2025-10-22 11:44:53 +01:00
Nawaz Dhandala
2204ebde26 feat(traces): add tabs for All/Root spans and apply root-span filtering in TraceTable 2025-10-22 11:41:35 +01:00
Nawaz Dhandala
57a8547e14 style(gantt): use conditional prop spreads and refine row/label highlight styling
- Replace direct optional prop passing with conditional spread for
  multiSelect and highlightBarIds in GanttChart Index, Rows and Row to
  avoid passing undefined values.
- Add labelPaddingLeft and adjust row layout: normalize borders, add
  transitions, spacing, overflow handling and subtle ring/shadow when
  highlighted.
- Pass isHighlighted into RowLabel and update RowLabel to apply
  highlighted title/description styles (font weight and colors).
- Normalize Bar isHighlighted check to a boolean expression for safety.
2025-10-22 11:30:35 +01:00
Nawaz Dhandala
53a70e1b93 feat(gantt,traces): add span highlighting support and propagate highlight state
- Add highlightBarIds prop to GanttChart and propagate through Rows -> RowIndex -> Row -> Bar
- Row: compute highlight set, detect highlighted descendants, auto-expand rows with highlights, and apply highlight styles (background, rounded container, data attribute)
- Bar: accept isHighlighted, apply highlight visuals (box-shadow, z-index) and adjust label styling
- Rows/RowIndex: include highlightBarIds in component interfaces and pass-through props
- TraceExplorer: accept highlightSpanIds, sanitize input, derive highlightableSpanIds from displayed spans, and include them in the generated GanttChartProps
- Trace view pages: extract spanId query param, normalize to highlightSpanIds, and pass to TraceExplorer

This enables highlighting specific spans (via spanId query) in the Gantt chart UI and ensures highlighted rows are visible and visually emphasized.
2025-10-22 11:22:32 +01:00
Nawaz Dhandala
3eb72ef7f9 style(telemetry,ingest,ui): normalize TelemetryUtil.getAttributeKeys call formatting and tidy LogItem JSX/whitespace 2025-10-22 11:03:26 +01:00
Nawaz Dhandala
2a471133c5 feat(logs): make span IDs clickable and refine trace link styling
- Add getSpanRoute prop to LogsViewer and LogItem and thread it through from callers.
- Implement renderSpanId to render clickable span links (resolves via getSpanRoute or by appending spanId to trace route).
- Show "No span" placeholder when spanId is missing.
- Prevent link clicks from toggling the log row (stopPropagation on span links).
- Tweak trace link decoration and external-link icon color for improved hover styling.
- Minor prop typing / pass-through cleanup in LogsViewer.
2025-10-22 11:02:07 +01:00
Nawaz Dhandala
34e92679b2 feat(logs): render trace IDs as clickable links with external-link icon and refined styling 2025-10-22 10:41:25 +01:00
Nawaz Dhandala
8b0c2a7320 feat(analytics): make trace IDs clickable in logs
Add an optional getTraceRoute prop to LogItem and LogsViewer and render traceId as a Link when a route is provided. Stop event propagation on link clicks to avoid toggling collapse. Wire getTraceRoute from Dashboard and Span viewers using RouteUtil.populateRouteParams.
2025-10-22 10:34:28 +01:00
Nawaz Dhandala
353d894394 feat(analytics): mark attributeKeys as required on Log, Metric and Span models 2025-10-21 22:22:48 +01:00
Nawaz Dhandala
09e4f0ff97 fix(analytics): cast metric.attributes when computing attributeKeys to satisfy TelemetryUtil signature 2025-10-21 22:10:46 +01:00
Nawaz Dhandala
247c4bc637 feat(analytics): populate attributeKeys on telemetry records and prefer attributeKeys column when querying
- Add TelemetryUtil.getAttributeKeys helper.
- Populate attributeKeys for Logs, Metrics, Spans, Alerts, Incidents and Monitor metrics during ingest/processing.
- Ensure FluentIngest initializes attributeKeys for raw logs.
- Update TelemetryAttributeService to use attributeKeys column when available and fall back to JSONExtractKeys(attrs).
- Improve attribute extraction SQL to filter empty/null attribute keys.
2025-10-21 22:05:48 +01:00
Nawaz Dhandala
72e2edd49d feat(analytics): add attributeKeys column to Log/Metric/Span, update projections and add migration 2025-10-21 22:04:49 +01:00
Nawaz Dhandala
1be494169d fix(analytics): rename ClickHouse projections to singular 'AttributeProjection' for Log, Metric and Span models 2025-10-21 21:05:52 +01:00
Nawaz Dhandala
3796053403 style(analytics): remove extra blank line in doesProjectionExist catch block 2025-10-21 21:04:17 +01:00
Nawaz Dhandala
f316bfb9fd feat(analytics): materialize projections after creation and add error handling
Call materializeProjection after creating a projection to ensure it is materialized.
Introduce materializeProjection to run ALTER TABLE ... MATERIALIZE PROJECTION and
add escapeIdentifier for safe identifier quoting. Wrap the projection-existence
query in try/catch with logging and rethrow on failure.
2025-10-21 21:03:10 +01:00
Nawaz Dhandala
40f9613bd3 chore(docker): bump clickhouse server image to 25.7 in docker-compose.base.yml 2025-10-21 20:49:33 +01:00
Nawaz Dhandala
a3bb9f003f style(analytics): add debug logging to projection processing in TableManegement.ts 2025-10-21 19:48:41 +01:00
Nawaz Dhandala
70714b2f21 feat(analytics): add ClickHouse projections for attributes to Log, Metric and Span models
Add projection definitions (ALTER TABLE ...) to Log, Metric and Span analytics models to extract distinct attribute keys per project (LogItemAttributesProjection, MetricItemAttributesProjection, SpanItemAttributesProjection).

style(telemetry): simplify empty query object in DeleteOldData.ts
2025-10-21 19:46:48 +01:00
Nawaz Dhandala
a62543bff0 feat(telemetry): iterate active projects and prune telemetry data per service
- Use ProjectService.getActiveProjectStatusQuery() to fetch active projects
- Query telemetry services per project and scope deletions by project.id
- Add try/catch blocks and logger calls for better error isolation and robustness
- Preserve default retention (15 days) and perform deletes for logs, spans, and metrics
2025-10-21 19:27:19 +01:00
Nawaz Dhandala
826a2006d0 style(telemetry): reformat retentionCutOff assignment in DeleteOldData.ts 2025-10-21 18:55:07 +01:00
Nawaz Dhandala
a4075fe349 feat(telemetry): compute single retention cutoff, prune by primary keys, and delete metrics
- compute retentionCutOff once per service instead of repeated calls
- use primary-key columns (time/startTime) for efficient pruning of logs and spans
- implement MetricService.deleteBy to remove old OpenTelemetry metrics
- minor import/order adjustments
2025-10-21 18:53:44 +01:00
Nawaz Dhandala
eb70a923f3 style(analytics): add explicit types for executeQuery result and json response in TableManegement.ts 2025-10-21 16:00:38 +01:00
Nawaz Dhandala
11d39898d3 style(analytics): normalize escaped variable formatting in TableManegement.ts 2025-10-21 15:54:45 +01:00
Nawaz Dhandala
8e5907d523 feat(analytics): check for existing projections before creating; add doesProjectionExist and escapeForQuery helpers and required imports 2025-10-21 15:51:07 +01:00
Nawaz Dhandala
c9e57fcb19 feat(analytics): add Projection type and use typed projections in models and table creation 2025-10-21 15:46:57 +01:00
Nawaz Dhandala
8c6bc331a4 style(analytics): normalize AnalyticsTableColumn constructor formatting
Reformat multi-line new AnalyticsTableColumn(...) expressions to a consistent style and tidy up minor inline description/spacing in ExceptionInstance, Log, Metric and Span models.
2025-10-21 14:56:01 +01:00
Nawaz Dhandala
20129e606a feat(analytics): add projections support to models and apply projection statements on table creation
- add projections param, backing field and accessors to AnalyticsBaseModel (defaults to [])
- update analytics models (ExceptionInstance, Log, Metric, MonitorLog, Span) to include projections: []
- execute each model.projections SQL statement in AnalyticsTableManagement when creating tables
2025-10-21 14:37:22 +01:00
Nawaz Dhandala
55e0eede68 refactor(analytics): extract AnalyticsTableColumn definitions into local variables
Move repeated new AnalyticsTableColumn(...) definitions into named constants and reuse them in tableColumns for Log, Metric, Span, MonitorLog and ExceptionInstance models. Reduces duplication and improves readability without changing behavior.
2025-10-21 14:18:57 +01:00
Nawaz Dhandala
ffa603503d fix: update image tag to use GitHub Container Registry for release builds 2025-10-21 11:19:04 +01:00
Nawaz Dhandala
18429caabe fix: refactor Microsoft Teams pagination logic to use configurable maximum pages 2025-10-20 15:31:18 +01:00
Simon Larsen
ccbfef1cfe Merge pull request #2039 from tollercode/fix/pagination-msteams-fetch-teams-list
fix: pagination msteams fetch teams list
2025-10-20 15:20:37 +01:00
Nils T
4789a15ce7 fix: make maximum pages for fetching teams configurable to prevent infinite loop 2025-10-20 12:47:13 +00:00
Nils T
9633307de0 fix: add pagination limit to prevent infinite loop when fetching teams from Microsoft Graph API 2025-10-20 12:43:24 +00:00
Nils T
ee5c2a0f33 fix: implement pagination for fetching all teams from Microsoft Graph API 2025-10-20 12:17:26 +00:00
snyk-bot
5c3b181507 fix: Probe/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PLAYWRIGHTCORE-13553173
2025-10-20 10:50:12 +00:00
Nawaz Dhandala
6bbf2f866c fix: update hard delete logic to retain items for 3 years instead of 120 days 2025-10-18 10:37:53 +01:00
Nawaz Dhandala
60e4b51ec9 fix: update Microsoft Teams app manifest version to 1.5.0 2025-10-17 10:43:32 +01:00
Nawaz Dhandala
f244f872d4 fix: improve formatting and consistency in tenant ID retrieval checks in MicrosoftTeamsUtil 2025-10-17 10:14:45 +01:00
Nawaz Dhandala
3e910c1308 fix: ensure tenant ID is retrieved and validated in MicrosoftTeamsUtil 2025-10-17 10:13:58 +01:00
Nawaz Dhandala
2b0067fd17 fix: update tenant ID retrieval logic in MicrosoftTeamsAPI 2025-10-17 10:11:54 +01:00
Nawaz Dhandala
cb23b6b55b Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-16 21:01:31 +01:00
Nawaz Dhandala
4406d52307 feat: add queries management section to Clickhouse documentation 2025-10-16 20:52:52 +01:00
Simon Larsen
03b6bec6d0 feat: add validation for required fields in WorkspaceProjectAuthTokenService and MicrosoftTeamsUtil 2025-10-16 20:36:45 +01:00
Nawaz Dhandala
ff0bd88b02 fix: ensure Microsoft Teams tenant ID is provided before creating Bot Framework adapter 2025-10-16 20:23:50 +01:00
Simon Larsen
94290c77db feat: add advanced filters toggle functionality across various components 2025-10-16 16:35:57 +01:00
Simon Larsen
003e44d331 refactor: clean up code formatting and improve readability in TelemetryAttributeService and LogsViewer components 2025-10-16 16:08:34 +01:00
Simon Larsen
b4cf798246 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-10-16 16:06:49 +01:00
Simon Larsen
e607367809 feat: enhance Filters and LogsViewer components with advanced filters functionality and loading states 2025-10-16 16:06:14 +01:00
Nawaz Dhandala
82a7b5794c fix: reduce row scan limit from 100000 to 10000 in TelemetryAttributeService 2025-10-16 15:59:28 +01:00
Nawaz Dhandala
37c1674029 fix: reduce cache stale time from 10 to 5 minutes in TelemetryAttributeService 2025-10-16 15:59:12 +01:00
Nawaz Dhandala
cd2a5222b8 fix: add expiration options for cache entries in GlobalCache and TelemetryAttributeService 2025-10-16 15:58:53 +01:00
Nawaz Dhandala
875d6c5df9 fix: enhance caching mechanism and improve attribute fetching in TelemetryAttributeService 2025-10-16 15:52:44 +01:00
Nawaz Dhandala
7eea90a55a fix: format constructor parameters for consistency in IdentityAPI 2025-10-16 15:42:13 +01:00
Nawaz Dhandala
c401b86a2e fix: update route handling to use Route class for URL construction across multiple services 2025-10-16 15:30:18 +01:00
Nawaz Dhandala
e96f781157 fix: refactor URL construction for view details and redirect routes in UserNotificationLogTimelineAPI 2025-10-16 15:05:57 +01:00
Nawaz Dhandala
73cc5f8f9e fix: update route handling in EnvironmentConfig for consistency 2025-10-16 15:04:52 +01:00
Nawaz Dhandala
0ee9876de9 fix: format code for consistency in OtelIngest services 2025-10-16 14:22:09 +01:00
Nawaz Dhandala
b76a75479d Implement OpenTelemetry Ingest Services for Logs, Metrics, and Traces
- Added OtelIngestBaseService as a base class for common functionality.
- Implemented OtelLogsIngestService to handle log ingestion, including processing and flushing logs to the database.
- Implemented OtelMetricsIngestService to handle metric ingestion, including processing and flushing metrics to the database.
- Implemented OtelTracesIngestService to handle trace ingestion, including processing spans and exceptions, and flushing them to the database.
- Introduced error handling and logging throughout the ingestion process to ensure robustness.
- Utilized CaptureSpan decorator for telemetry tracking in key methods.
- Enhanced service name extraction from request attributes for better traceability.
2025-10-16 14:21:44 +01:00
Simon Larsen
79b9cf4c06 Merge pull request #2034 from OneUptime/telemetry-attr
Telemetry attr
2025-10-15 18:13:17 +01:00
Nawaz Dhandala
a6da59c966 fix: add Results and DbJSONResponse types to AnalyticsDatabaseService and update TelemetryAttributeService to use them 2025-10-15 18:12:34 +01:00
Nawaz Dhandala
b84695feb9 fix: remove TelemetryType references and indexAttributes calls from various services 2025-10-15 17:21:56 +01:00
Nawaz Dhandala
b77973441d fix: remove TelemetryAttribute and TelemetryAttributeService references and related code 2025-10-15 16:26:05 +01:00
Nawaz Dhandala
27e9c07c57 fix: remove padding from MarkdownViewer component 2025-10-15 15:57:45 +01:00
Nawaz Dhandala
cbf8684d8c fix: remove force garbage collection method and its calls from OtelIngestService 2025-10-15 15:47:22 +01:00
Nawaz Dhandala
87057757a5 fix: remove --expose-gc option from NODE_OPTIONS in Dockerfile, nodemon.json, and package.json 2025-10-15 15:42:29 +01:00
Nawaz Dhandala
23b587f0f6 fix: remove unused EnableWorkflow import from TelemetryException model 2025-10-15 15:22:53 +01:00
Nawaz Dhandala
81c7a4eeb7 fix: remove EnableWorkflow decorator from TelemetryException model 2025-10-15 14:49:56 +01:00
Nawaz Dhandala
549dbfd6c7 fix: format nodemon.json and adjust package.json dev script indentation 2025-10-15 13:45:54 +01:00
Nawaz Dhandala
a2eac673eb fix: update NODE_OPTIONS to include --expose-gc and --use-openssl-ca in Dockerfile and nodemon configuration 2025-10-15 13:45:07 +01:00
Nawaz Dhandala
086a0a661d feat: implement dynamic batch size configuration for OpenTelemetry ingestion 2025-10-15 13:39:11 +01:00
Nawaz Dhandala
da0620eafa fix: improve logging of request body in OpenTelemetry middleware 2025-10-15 12:53:15 +01:00
Nawaz Dhandala
ca90ab0db4 fix: update Kubernetes cheatsheet to include OOMKilled pods in cleanup section 2025-10-15 12:36:06 +01:00
Nawaz Dhandala
94dacc20db feat: add Kubernetes cheatsheet for pod cleanup commands 2025-10-15 12:30:34 +01:00
Nawaz Dhandala
7e887bd4cd feat: enhance app description with HTML formatting and update support links 2025-10-14 19:37:28 +01:00
Nawaz Dhandala
b50dfcdf1c fix: update Microsoft Teams setup guide URL to correct link 2025-10-14 19:02:34 +01:00
Nawaz Dhandala
e2bc0ea4aa feat: add welcome card state management to prevent duplicate sends 2025-10-14 19:02:06 +01:00
Simon Larsen
7d018b94d3 Merge pull request #2033 from OneUptime/fix-change-plan
Fix change plan
2025-10-14 14:12:05 +01:00
Nawaz Dhandala
9fb7a70dc9 feat: add createOrUpdateApiUrl prop to CardModelDetail and update Billing page to generate change plan API URL 2025-10-14 14:02:45 +01:00
Nawaz Dhandala
a37b3fc0b3 feat: implement change plan API with billing permission checks 2025-10-14 13:54:38 +01:00
Nawaz Dhandala
c06ef5ddfc fix: add ManageProjectBilling permission for project updates 2025-10-14 13:34:12 +01:00
Nawaz Dhandala
0fb7174e94 chore: remove unused devDependencies from package.json and package-lock.json 2025-10-14 11:59:17 +01:00
Nawaz Dhandala
84a7cd976d fix: update template IDs for AlertCreated and IncidentCreated in WhatsAppTemplates 2025-10-14 10:44:27 +01:00
Nawaz Dhandala
f7c8c00f04 t psh
:wq!
Merge branch 'master' into release
2025-10-13 13:33:02 +01:00
Simon Larsen
51c3fcd3ca Merge pull request #2032 from OneUptime/whatsapp-webhook
Whatsapp webhook
2025-10-13 13:32:25 +01:00
Nawaz Dhandala
74c3dde7f1 refactor: improve type annotations and enhance readability in WhatsApp API and LogsTable components 2025-10-13 13:31:56 +01:00
Nawaz Dhandala
6bf45f6f31 refactor: streamline code formatting and improve readability in WhatsApp components and services 2025-10-13 13:26:57 +01:00
Nawaz Dhandala
4de4ad8022 feat: enhance status color handling in WhatsAppLogsTable component 2025-10-13 13:26:19 +01:00
Nawaz Dhandala
e263900115 feat: add migration for metaWhatsAppWebhookVerifyToken in GlobalConfig 2025-10-13 13:15:57 +01:00
Nawaz Dhandala
34aaa34fb3 refactor: remove shorthand webhook URL and simplify callback URL instructions in WhatsApp setup markdown 2025-10-13 13:14:10 +01:00
Nawaz Dhandala
9f72a8e554 feat: add webhook verification and enhance WhatsApp status handling 2025-10-13 12:44:48 +01:00
Nawaz Dhandala
8dfabfd96f feat: enhance WhatsApp status handling and add webhook verify token to GlobalConfig 2025-10-13 12:33:39 +01:00
Nawaz Dhandala
14cdc3ea86 chore: update package.json and package-lock.json to add ts-jest and new dependencies 2025-10-13 12:31:52 +01:00
Nawaz Dhandala
cd6abe63ea refactor: format code and ensure consistent import/export statements across migration and mobile app files 2025-10-13 11:52:41 +01:00
Nawaz Dhandala
aeb6d53b9d refactor: remove unused imports and simplify status message handling in WhatsAppLogsTable 2025-10-13 11:41:29 +01:00
Nawaz Dhandala
efa7224718 fix: update ConfirmModal description styling for better text wrapping 2025-10-13 10:56:46 +01:00
Nawaz Dhandala
a2c406d7cc refactor: simplify database properties handling in WhatsAppLogAPI 2025-10-13 10:51:52 +01:00
Nawaz Dhandala
1933e37beb refactor: move getMetaWhatsAppConfig call to improve message handling logic 2025-10-13 10:43:46 +01:00
Nawaz Dhandala
0b0336f9ea Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-13 10:35:51 +01:00
Simon Larsen
9304079a1c feat: implement WhatsAppLogAPI for fetching WhatsApp message status and update WhatsAppLogsTable to display message ID 2025-10-13 10:35:41 +01:00
Nawaz Dhandala
1e2dcf332b fix: enhance type handling for return values in VM router 2025-10-13 10:15:46 +01:00
Simon Larsen
ca860f54a8 feat: update permissions to replace ReadSmsLog with ReadWhatsAppLog in WhatsAppLog model and Permission types 2025-10-13 10:01:05 +01:00
Simon Larsen
513e4146ed feat: add migration for WhatsAppMessageId field in WhatsAppLog table with index 2025-10-13 09:59:33 +01:00
Simon Larsen
43d31ddbe9 feat: add WhatsAppMessageId field to WhatsAppLog model with access control and metadata 2025-10-13 09:55:22 +01:00
Simon Larsen
18231f42aa feat: update MobileAppPrompt.md for UI guidelines and add react-native-webview dependency in package.json; create AuthSession type 2025-10-12 12:38:36 +01:00
Simon Larsen
60291cc218 docs: update MobileAppPrompt.md to clarify usage of @oneuptime/common package 2025-10-12 12:33:46 +01:00
Simon Larsen
3658f0349e feat: add react-native-worklets dependency to package.json and package-lock.json 2025-10-12 12:29:29 +01:00
Simon Larsen
02133165d8 chore: update dependencies and configure Tailwind CSS with NativeWind
- Added @expo/metro-runtime, nativewind, prettier-plugin-tailwindcss, react-dom, and tailwindcss to dependencies.
- Updated react-native-safe-area-context to version 5.4.0.
- Added global.css for Tailwind CSS styles.
- Created metro.config.js to integrate NativeWind with Expo.
- Added nativewind-env.d.ts for TypeScript support.
- Created tailwind.config.js to configure Tailwind CSS with NativeWind.
- Updated tsconfig.json to include nativewind-env.d.ts.
2025-10-12 12:28:30 +01:00
Simon Larsen
837b841352 refactor: remove unused ObjectID generation and related state management 2025-10-12 10:29:00 +01:00
Simon Larsen
10e344dad5 feat: add react-native-get-random-values dependency and import in App component 2025-10-12 10:08:23 +01:00
Simon Larsen
848bfb358f chore: add @oneuptime/common dependency and update tsconfig to exclude Common directory 2025-10-12 10:05:18 +01:00
Simon Larsen
2ca1c64532 Implement new feature for user authentication and improve error handling 2025-10-12 09:55:28 +01:00
Simon Larsen
d9a79eafbd fix: clarify file naming conventions in mobile app development prompt 2025-10-12 09:26:31 +01:00
Simon Larsen
a1dc16f4b6 feat: refactor styles into separate Styles.ts file for better organization 2025-10-12 09:25:23 +01:00
Nawaz Dhandala
674613c0d6 feat: add initial mobile app development prompt in MobileAppPrompt.md 2025-10-12 09:20:09 +01:00
Nawaz Dhandala
e4e095798a fix: enhance type definitions and import statements in App.tsx 2025-10-11 12:22:15 +01:00
Nawaz Dhandala
2e07185584 fix: standardize formatting in App.tsx and tsconfig.json 2025-10-11 12:19:33 +01:00
Nawaz Dhandala
144001981a Add initial configuration for mobile app with Expo
- Created package.json for project dependencies and scripts
- Added TypeScript configuration with strict settings and React Native support
2025-10-11 12:16:39 +01:00
Nawaz Dhandala
cd096f66ea fix: update app approval status message to include manual sideloading instructions 2025-10-11 12:15:52 +01:00
Nawaz Dhandala
036b29da51 fix: standardize error messages and improve formatting in MicrosoftTeamsAPI 2025-10-11 11:12:49 +01:00
Nawaz Dhandala
d0d59147ae fix: add download button for app manifest and manual sideloading instructions 2025-10-11 11:12:15 +01:00
snyk-bot
565fbe6cd3 fix: Nginx/Dockerfile.tpl to reduce vulnerabilities 2025-10-11 08:37:10 +00:00
snyk-bot
12c3f9b25c fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NODEMAILER-13378253
2025-10-11 08:35:50 +00:00
Nawaz Dhandala
02cffe9f8b fix: update Microsoft Teams outline image 2025-10-10 20:39:12 +01:00
Nawaz Dhandala
9c52e8966f fix: update Microsoft Teams outline image 2025-10-10 17:38:24 +01:00
Nawaz Dhandala
9257a949fe fix: add Microsoft Teams app client ID and secret placeholders to config example 2025-10-10 16:34:25 +01:00
Nawaz Dhandala
7528ed8e0c fix: update Microsoft Teams app manifest version and improve error handling 2025-10-10 15:55:18 +01:00
Nawaz Dhandala
98b6f3eac3 fix: streamline text formatting in Microsoft Teams integration messages 2025-10-10 09:17:34 +01:00
Nawaz Dhandala
a2561b3ae8 fix: update acknowledge route to acknowledge-page in UserNotificationRuleService 2025-10-10 09:15:55 +01:00
Nawaz Dhandala
368f5c6bbc Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-10 09:01:23 +01:00
Nawaz Dhandala
0e2e30b0a9 fix: remove Microsoft identity association endpoint for cleanup 2025-10-10 09:00:42 +01:00
Simon Larsen
2664a24875 fix: update outline image for Microsoft Teams integration 2025-10-09 18:47:58 +01:00
Simon Larsen
40e486669f feat: enhance app description and support information in Microsoft Teams API 2025-10-09 18:36:45 +01:00
Simon Larsen
ae64cbc718 feat: implement welcome adaptive card for OneUptime bot in Microsoft Teams 2025-10-09 18:35:32 +01:00
Simon Larsen
e14f691cc4 feat: add comment for Microsoft identity verification endpoint 2025-10-09 18:21:40 +01:00
Simon Larsen
89c607a530 feat: add endpoint for Microsoft identity association JSON 2025-10-09 18:21:03 +01:00
Simon Larsen
90f4e7418f fix: streamline SCIM group schema definition and ensure proper syntax in migrations 2025-10-09 17:40:44 +01:00
Simon Larsen
79fed2da09 fix: remove deprecated SCIM 1.1 schema references for improved compatibility 2025-10-09 17:34:37 +01:00
Simon Larsen
1dd41c103f Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-10-09 17:34:18 +01:00
Simon Larsen
988e85affc fix: remove deprecated SCIM schema reference in formatTeamForSCIM function 2025-10-09 17:34:15 +01:00
Nawaz Dhandala
2810516987 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-09 17:19:00 +01:00
Nawaz Dhandala
679c1971a2 fix: update WhatsApp setup instructions for token generation and template creation 2025-10-09 17:18:58 +01:00
Nawaz Dhandala
3abdbb7ef9 fix: correct typo in IncidentStateChangedOwnerNotification template ID 2025-10-09 17:12:17 +01:00
Simon Larsen
0d647f5dc1 fix: improve release retrieval by using pagination and handle missing release tags 2025-10-09 13:43:43 +01:00
Nawaz Dhandala
bd1df491a2 Remove MobileApp components, themes, and configuration files
- Deleted package.json, removing all dependencies and scripts for the mobile app.
- Removed FeatureCard, GlowingButton, MetricCard, SafeAreaContainer, and HomeScreen components.
- Eliminated color, layout, and typography theme files.
- Deleted TypeScript configuration file (tsconfig.json).
2025-10-09 11:59:34 +01:00
Nawaz Dhandala
feb872d05c feat: add E2E testing support with docker-compose configuration 2025-10-09 11:39:30 +01:00
Nawaz Dhandala
64b6b99a21 refactor: replace retry action with shell commands for preinstall and E2E tests 2025-10-08 20:10:19 +01:00
Nawaz Dhandala
8046c244b1 feat: add react-dom and react-native-web dependencies to package.json 2025-10-08 18:52:29 +01:00
Nawaz Dhandala
6928316ba0 feat: implement initial structure and components for OneUptime Mobile app 2025-10-08 18:33:50 +01:00
Nawaz Dhandala
36c74642f2 Initialize MobileApp with package.json and tsconfig.json for Expo project setup 2025-10-08 18:14:29 +01:00
Nawaz Dhandala
5a992e99c8 refactor: format publisherDocsUrl for better readability in Microsoft Teams app manifest 2025-10-08 18:06:52 +01:00
Nawaz Dhandala
ff9230f878 refactor: add publisherDocsUrl to Microsoft Teams app manifest 2025-10-08 17:08:45 +01:00
Nawaz Dhandala
9afa861ff2 refactor: remove obsolete migration files and update index 2025-10-08 17:04:59 +01:00
Nawaz Dhandala
6d7486e76d refactor: update developer name from "OneUptime" to "HackerBay Inc" in MicrosoftTeamsAPI 2025-10-08 17:02:59 +01:00
Nawaz Dhandala
8529012b19 refactor: remove "whatsAppText" column from WhatsAppLog table 2025-10-08 16:45:29 +01:00
Nawaz Dhandala
f704bd47a3 refactor: remove unused column "whatsAppText" from WhatsAppLog table 2025-10-08 14:58:24 +01:00
Nawaz Dhandala
239f2fc34e refactor: streamline actionLink resolution in createWhatsAppMessageFromTemplate 2025-10-08 14:35:31 +01:00
Nawaz Dhandala
5d5183b08e refactor: remove action_link from templateVariables in createWhatsAppMessageFromTemplate 2025-10-08 14:29:13 +01:00
Nawaz Dhandala
7cad0fab0f refactor: clean up whitespace and improve code formatting in multiple files 2025-10-08 14:20:41 +01:00
Nawaz Dhandala
98e0d55ee3 feat: update template variable iteration to include parameter names 2025-10-08 14:19:01 +01:00
Nawaz Dhandala
2d8f0d7a58 feat: add recipient_type to WhatsApp message payload for individual recipients 2025-10-08 13:46:55 +01:00
Nawaz Dhandala
643303cd7a feat: enhance error handling to include HTTPErrorResponse support 2025-10-08 13:27:26 +01:00
snyk-bot
9ee0e0f3cb fix: Common/package.json & Common/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NODEMAILER-13378253
2025-10-08 05:10:50 +00:00
Nawaz Dhandala
dc692203be feat: add selectMoreFields for delivery channel options in notification settings 2025-10-07 22:40:29 +01:00
Nawaz Dhandala
648c51d007 feat: enhance notification settings with delivery channel options and icons 2025-10-07 22:37:40 +01:00
Nawaz Dhandala
0e5b106333 fix: update icon for resend code button from WhatsApp to SMS 2025-10-07 22:20:24 +01:00
Nawaz Dhandala
40e9ea2ab6 feat: add WhatsApp alert options to notification settings 2025-10-07 22:19:16 +01:00
Nawaz Dhandala
2157e228b9 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-07 21:49:36 +01:00
Nawaz Dhandala
5c5534adb8 fix: add type annotation for response in sendWhatsAppMessage method 2025-10-07 21:49:34 +01:00
Nawaz Dhandala
379297cd7e fix: streamline async function signatures in API routes for consistency 2025-10-07 21:49:14 +01:00
Nawaz Dhandala
b3730e9708 fix: add user authorization middleware to /test endpoint 2025-10-07 21:48:31 +01:00
Nawaz Dhandala
2fbc44d5c3 Refactor API endpoints to include error handling middleware
- Updated multiple API routes to use NextFunction for error handling.
- Wrapped asynchronous route handlers in try-catch blocks to catch and pass errors to the next middleware.
- Ensured consistent error responses across various endpoints, improving maintainability and readability.
- Enhanced the structure of the code by reducing nested try-catch blocks and improving flow control.
2025-10-07 21:46:25 +01:00
Nawaz Dhandala
12f05937af fix: enhance error handling in API endpoints by adding NextFunction support 2025-10-07 21:17:59 +01:00
Simon Larsen
5ea440492b Merge branch 'master' of github.com:OneUptime/oneuptime 2025-10-07 21:16:14 +01:00
Simon Larsen
57b851a498 refactor: rename github-release job to draft-github-release and add finalize-github-release job for publishing 2025-10-07 21:15:45 +01:00
Nawaz Dhandala
00f806b077 fix: enhance error handling in WhatsApp message template rendering for missing variables 2025-10-07 21:13:40 +01:00
Nawaz Dhandala
975c20a788 fix: improve error handling in verification endpoints by adding NextFunction support 2025-10-07 20:49:27 +01:00
Nawaz Dhandala
948e2d93c1 fix: handle errors in resend verification code endpoint and add NextFunction support 2025-10-07 20:38:11 +01:00
Nawaz Dhandala
6de3c93745 fix: refactor error handling for WhatsApp verification and resend code modals 2025-10-07 20:31:47 +01:00
Nawaz Dhandala
1f63110561 feat: enhance WhatsApp verification code handling with error logging and improved response management 2025-10-07 20:25:32 +01:00
Nawaz Dhandala
8a94c35450 feat: enforce template key requirement for WhatsApp messages and add debug logging for API requests 2025-10-07 16:59:53 +01:00
Nawaz Dhandala
fdc97284a5 fix: update DEFAULT_META_WHATSAPP_API_VERSION to v23.0 2025-10-07 16:48:22 +01:00
Nawaz Dhandala
792ecfdbdc fix: remove 'From' column from WhatsApp logs table 2025-10-07 16:04:24 +01:00
Nawaz Dhandala
121b01dc08 feat: add WhatsApp notifications toggle to notification settings 2025-10-07 16:01:27 +01:00
Nawaz Dhandala
1aa49071eb feat: add UserWhatsAppAPI for handling WhatsApp verification and resend functionality 2025-10-07 15:57:06 +01:00
Nawaz Dhandala
66bef1284a feat: add documentation for canceling all GitHub actions jobs using GitHub CLI 2025-10-07 15:53:37 +01:00
Nawaz Dhandala
1526f708de fix: enhance error handling and logging for WhatsApp message sending failures 2025-10-07 15:49:34 +01:00
Nawaz Dhandala
53198e4486 fix: improve error logging for WhatsApp message sending failures 2025-10-07 15:39:10 +01:00
Nawaz Dhandala
a66bf2df2a feat: enhance WhatsApp icon SVG with stroke properties and viewBox 2025-10-07 15:31:20 +01:00
Nawaz Dhandala
34422721c3 feat: add WhatsApp logs table to Notification Logs 2025-10-07 15:19:36 +01:00
Nawaz Dhandala
b5008c2363 Merge branch 'whatsapp' 2025-10-07 15:15:48 +01:00
Nawaz Dhandala
1cbab2be08 refactor: remove unused dashboard link and update WhatsApp message template variables 2025-10-07 15:15:25 +01:00
Simon Larsen
1bdcfd71f7 Merge pull request #2026 from OneUptime/whatsapp
Whatsapp
2025-10-07 15:05:58 +01:00
Nawaz Dhandala
792271b146 Merge branch 'whatsapp' of https://github.com/OneUptime/oneuptime into whatsapp 2025-10-07 15:02:44 +01:00
Nawaz Dhandala
3de5fbd35c fix: correct variable assignment for templateKey in test message route 2025-10-07 15:02:19 +01:00
Nawaz Dhandala
6f8dc1ed59 feat: update test message sending functionality to use predefined template and language 2025-10-07 15:00:41 +01:00
Nawaz Dhandala
92309d8fb2 feat: add test notification template and implement test message sending functionality in WhatsApp setup 2025-10-07 14:57:00 +01:00
Simon Larsen
d3070975cb Merge branch 'whatsapp' of github.com:OneUptime/oneuptime into whatsapp 2025-10-07 14:37:26 +01:00
Simon Larsen
69c0da5b17 refactor: remove WhatsApp high-risk cost variable and update related configurations 2025-10-07 14:37:23 +01:00
Nawaz Dhandala
1736690f01 feat: enhance WhatsApp setup markdown with detailed instructions for Business Account ID, App ID, and App Secret 2025-10-07 14:22:43 +01:00
Nawaz Dhandala
78a92905e9 feat: update WhatsApp setup markdown for clarity and improved instructions 2025-10-07 14:21:24 +01:00
Nawaz Dhandala
801c1b4ccb feat: remove header from WhatsApp setup markdown for improved clarity 2025-10-07 14:15:37 +01:00
Nawaz Dhandala
703b1dca51 feat: enhance WhatsApp setup markdown formatting for better structure and clarity 2025-10-07 14:12:55 +01:00
Nawaz Dhandala
e5ef97abd9 feat: improve WhatsApp setup markdown formatting for clarity and structure 2025-10-07 13:58:28 +01:00
Nawaz Dhandala
3053856990 feat: enhance WhatsApp setup markdown formatting for improved readability 2025-10-07 13:45:12 +01:00
Nawaz Dhandala
b8e82e2801 feat: add WhatsApp configuration fields to GlobalConfig migration and update template IDs for type safety 2025-10-07 13:36:21 +01:00
Nawaz Dhandala
2187fe63f0 feat: refactor WhatsApp template IDs and messages for improved type safety and export structure 2025-10-07 13:29:36 +01:00
Nawaz Dhandala
b1f842f9e1 feat: enhance WhatsApp setup documentation with prerequisites and structured steps 2025-10-07 13:25:52 +01:00
Nawaz Dhandala
43e03fb61c feat: update WhatsApp icon SVG path for improved rendering 2025-10-07 13:19:29 +01:00
Nawaz Dhandala
2327ab84c2 feat: add migration for WhatsApp configuration fields in GlobalConfig 2025-10-07 13:16:19 +01:00
Nawaz Dhandala
93034b6018 refactor: update WhatsApp migration and enhance type definitions in WhatsApp settings 2025-10-07 13:10:54 +01:00
Nawaz Dhandala
e45022b5cb feat: add migration for WhatsApp notifications and related database changes 2025-10-07 13:06:47 +01:00
Nawaz Dhandala
c022b70e6d Merge branch 'whatsapp' of https://github.com/OneUptime/oneuptime into whatsapp 2025-10-07 13:06:11 +01:00
Nawaz Dhandala
5615ba2df7 Merge branch 'master' into whatsapp 2025-10-07 13:05:46 +01:00
Nawaz Dhandala
268960bc5b refactor: simplify destructuring of connection settings in getTransporter method 2025-10-07 12:58:19 +01:00
Nawaz Dhandala
508a713ecf refactor: enhance connection handling in TransporterPool for improved email server configuration 2025-10-07 12:57:43 +01:00
Simon Larsen
36e50b591a refactor: update WhatsApp template messages for improved clarity and conciseness 2025-10-07 12:17:04 +01:00
Nawaz Dhandala
05c1f95ba4 fix: enable tool-cache in release workflow for improved build efficiency 2025-10-07 11:52:13 +01:00
Simon Larsen
9d355691ae refactor: rename scheduled_maintenance_number to scheduled_event_number in WhatsApp templates and notification jobs for consistency 2025-10-07 11:32:54 +01:00
Simon Larsen
73c58186b6 refactor: enhance WhatsApp template messages with scheduled maintenance number for better context 2025-10-06 20:23:11 +01:00
Simon Larsen
eb8d3e4dfd refactor: update WhatsApp template messages for clarity and conciseness 2025-10-06 20:17:38 +01:00
Nawaz Dhandala
987f30e5c7 feat: add PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD environment variable to Dockerfiles for improved build performance 2025-10-06 19:45:46 +01:00
Simon Larsen
b7a0dbf81b refactor: replace action_link with corresponding link variables in notification templates 2025-10-06 18:25:59 +01:00
Simon Larsen
7368a0eb7c Refactor notification links in WhatsApp templates and services
- Updated variable names for links in various services to improve consistency:
  - Changed `*_link_on_dashboard` to `*_link` across multiple services including MonitorService, OnCallDutyPolicyEscalationRuleService, and UserNotificationRuleService.

- Modified WhatsApp template messages to reflect the new link variable names, ensuring that the notifications sent to users contain the correct links.

- Adjusted the link variable names in the WhatsAppTemplateUtil to match the updated naming convention.

- Ensured all related notification jobs (e.g., SendCreatedResourceNotification, SendNotePostedNotification) are updated to use the new link variables for better clarity and maintainability.
2025-10-06 18:07:39 +01:00
Nawaz Dhandala
ef0b6f3e14 refactor: replace docker login actions with retry mechanism for improved reliability 2025-10-06 16:36:28 +01:00
Simon Larsen
af8691f61d refactor: remove Meta WhatsApp API version handling and update documentation 2025-10-06 16:31:29 +01:00
Simon Larsen
f7aee2e253 feat: add Meta WhatsApp settings page and configuration options 2025-10-06 16:26:05 +01:00
Nawaz Dhandala
0a1b74d911 fix: update @simplewebauthn/server to version 13.2.2 in package.json and package-lock.json 2025-10-06 16:13:53 +01:00
Nawaz Dhandala
ab48da447d refactor: add test jobs to push-release-tags workflow for enhanced testing coverage 2025-10-06 16:08:43 +01:00
Nawaz Dhandala
1cc0630939 feat: add APP_TAG pinning to versioned release in config.env 2025-10-06 16:06:16 +01:00
Simon Larsen
5391ff4688 Add dashboard links to WhatsApp notifications for various resources
- Added `monitor_link_on_dashboard` to MonitorService notifications.
- Added `on_call_policy_link_on_dashboard` to OnCallDutyPolicyEscalationRule services.
- Added `on_call_schedule_link_on_dashboard` to OnCallDutyPolicyScheduleService.
- Added `probe_link_on_dashboard` to ProbeService notifications.
- Enhanced UserNotificationRuleService to include `alert_link_on_dashboard` and `incident_link_on_dashboard`.
- Updated WhatsApp templates to include links to the OneUptime dashboard for alerts, incidents, monitors, probes, scheduled maintenance, and status pages.
- Added `status_page_link_on_dashboard` to StatusPageOwners notifications.
2025-10-06 15:56:12 +01:00
Nawaz Dhandala
f6d96676fe refactor: update dependencies for test-e2e-release-saas job to include publish-mcp-server 2025-10-06 15:54:50 +01:00
Simon Larsen
cf02842ab1 Refactor WhatsApp message handling across services
- Updated `createWhatsAppMessageFromTemplate` to return a typed `WhatsAppMessagePayload`.
- Added type annotations for `WhatsAppTemplateId` in various services to improve type safety.
- Refactored WhatsApp message creation in `ProbeService`, `UserNotificationRuleService`, `UserNotificationSettingService`, and other worker jobs to ensure consistent typing.
- Enhanced readability and maintainability by using explicit types for event types and WhatsApp message payloads.
- Improved the structure of WhatsApp template ID definitions for better clarity and type inference.
2025-10-06 15:12:29 +01:00
Nawaz Dhandala
b63fcf6b99 refactor: update push-release-tags job dependencies for improved workflow order 2025-10-06 14:54:08 +01:00
Nawaz Dhandala
36069c1b4e refactor: update job dependencies for push-release-tags and github-release in release workflow 2025-10-06 14:48:05 +01:00
Simon Larsen
d2846decce refactor: improve code formatting and readability across multiple files 2025-10-06 14:47:15 +01:00
Simon Larsen
60a8a3f052 feat: include alert and incident identifiers in SMS notifications for better context 2025-10-06 14:45:46 +01:00
Simon Larsen
e2d15dc2e7 Merge branch 'master' of github.com:OneUptime/oneuptime into whatsapp 2025-10-06 14:39:00 +01:00
Simon Larsen
7cdefdeccd feat: add incident and alert numbers to WhatsApp notification templates and related services 2025-10-06 14:37:45 +01:00
Simon Larsen
684b8822af feat: simplify WhatsApp template messages for clarity and user engagement 2025-10-06 14:20:52 +01:00
Nawaz Dhandala
231bc47942 refactor: improve type annotations and formatting in TeamService for clarity 2025-10-06 14:14:11 +01:00
Nawaz Dhandala
965a497be3 feat: implement SCIM mutation checks for team creation and deletion 2025-10-06 14:12:19 +01:00
Nawaz Dhandala
f50a7fb99b refactor: adjust job dependencies and increase timeout for E2E tests in release workflows 2025-10-06 14:08:50 +01:00
Simon Larsen
50a5e75d1a feat: enhance WhatsApp template messages with additional information for user guidance 2025-10-06 14:00:01 +01:00
Simon Larsen
84e838a055 refactor: streamline WhatsApp message creation by removing unused template string imports 2025-10-04 13:49:12 +01:00
Simon Larsen
6d6c78e974 feat: Integrate WhatsApp notifications for various alert and incident events
- Added WhatsApp message creation for alert owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Implemented WhatsApp message functionality for incident owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Enhanced monitor owner notifications with WhatsApp messages in SendCreatedResourceNotification, SendOwnerAddedNotification, and SendStatusChangeNotification jobs.
- Included WhatsApp message creation for scheduled maintenance owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Added WhatsApp message functionality for status page owner notifications in SendAnnouncementCreatedNotification, SendCreatedResourceNotification, and SendOwnerAddedNotification jobs.
- Introduced WhatsAppTemplateUtil to manage WhatsApp message templates and creation logic.
2025-10-04 13:22:56 +01:00
Simon Larsen
778d5b7c6b feat: add UserWhatsAppService and integrate WhatsApp verification code template 2025-10-03 19:25:53 +01:00
Nawaz Dhandala
8051146f41 refactor: enhance type safety and improve variable naming in OnCallDutyScheduleSettings 2025-10-03 19:19:46 +01:00
Simon Larsen
86a359a230 feat: enhance WhatsApp notification handling with template support and error logging 2025-10-03 19:17:10 +01:00
Simon Larsen
c16dac65cc feat: add WhatsApp template IDs and rendering logic for notifications 2025-10-03 19:16:04 +01:00
Simon Larsen
437c9ecdbc feat: add support for WhatsApp notifications in project and user notification settings 2025-10-03 19:04:33 +01:00
Nawaz Dhandala
bf4eec2bdf refactor: improve code formatting and comments for better readability in SCIM and TimePicker components 2025-10-03 19:00:49 +01:00
Nawaz Dhandala
08367f3c7f refactor: update onDuplicateSuccess type to support Promise and implement duplication logic in OnCallDutyScheduleSettings 2025-10-03 18:23:29 +01:00
Simon Larsen
f5d077956a feat: integrate UserWhatsApp into notification settings and management 2025-10-03 18:12:07 +01:00
Simon Larsen
ca74005262 feat: add WhatsApp phone display in NotificationMethodView and create WhatsApp management component 2025-10-03 18:11:10 +01:00
Simon Larsen
52c936935e feat: add UserWhatsApp relation and ID to UserOnCallLogTimeline model 2025-10-03 17:51:34 +01:00
Simon Larsen
2951600ed9 feat: add UserWhatsApp relation and ID to UserNotificationRule model 2025-10-03 17:51:15 +01:00
Simon Larsen
d12c8c778c feat: Add UserWhatsApp and WhatsAppLog models with associated services
- Created UserWhatsApp model to manage WhatsApp numbers linked to users and projects.
- Implemented WhatsAppLog model to log messages sent via WhatsApp, including details like recipient, status, and associated incidents or alerts.
- Developed WhatsAppLogService for managing WhatsApp log entries, including automatic deletion of old logs if billing is enabled.
- Introduced WhatsAppService for sending WhatsApp messages with various contextual options.
2025-10-03 17:28:31 +01:00
Simon Larsen
77d4527a00 feat: add WhatsApp integration with API and configuration support 2025-10-03 17:24:59 +01:00
Nawaz Dhandala
1ef3353155 fix: update modelId retrieval to correctly use parameter for OnCallDutyScheduleSettings 2025-10-03 17:11:04 +01:00
Nawaz Dhandala
2c635c0d1e refactor: add modulePathIgnorePatterns to jest config for build directory 2025-10-03 16:17:45 +01:00
snyk-bot
50be2a666c fix: Probe/package.json & Probe/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-12613773
2025-09-15 08:22:25 +00:00
634 changed files with 106510 additions and 42376 deletions

View File

@@ -31,7 +31,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Accounts/Dockerfile .
command: sudo docker build --no-cache -f ./Accounts/Dockerfile .
docker-build-isolated-vm:
runs-on: ubuntu-latest
@@ -54,7 +54,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./IsolatedVM/Dockerfile .
command: sudo docker build --no-cache -f ./IsolatedVM/Dockerfile .
docker-build-home:
runs-on: ubuntu-latest
@@ -77,7 +77,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Home/Dockerfile .
command: sudo docker build --no-cache -f ./Home/Dockerfile .
docker-build-worker:
runs-on: ubuntu-latest
@@ -100,7 +100,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Worker/Dockerfile .
command: sudo docker build --no-cache -f ./Worker/Dockerfile .
docker-build-workflow:
runs-on: ubuntu-latest
@@ -123,7 +123,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Workflow/Dockerfile .
command: sudo docker build --no-cache -f ./Workflow/Dockerfile .
docker-build-api-reference:
runs-on: ubuntu-latest
@@ -146,7 +146,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./APIReference/Dockerfile .
command: sudo docker build --no-cache -f ./APIReference/Dockerfile .
docker-build-docs:
runs-on: ubuntu-latest
@@ -169,7 +169,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Docs/Dockerfile .
command: sudo docker build --no-cache -f ./Docs/Dockerfile .
docker-build-otel-collector:
@@ -193,7 +193,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./OTelCollector/Dockerfile .
command: sudo docker build --no-cache -f ./OTelCollector/Dockerfile .
docker-build-app:
runs-on: ubuntu-latest
@@ -217,7 +217,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./App/Dockerfile .
command: sudo docker build --no-cache -f ./App/Dockerfile .
docker-build-copilot:
@@ -241,7 +241,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Copilot/Dockerfile .
command: sudo docker build --no-cache -f ./Copilot/Dockerfile .
docker-build-e2e:
runs-on: ubuntu-latest
@@ -265,7 +265,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./E2E/Dockerfile .
command: sudo docker build --no-cache -f ./E2E/Dockerfile .
docker-build-admin-dashboard:
runs-on: ubuntu-latest
@@ -288,7 +288,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./AdminDashboard/Dockerfile .
command: sudo docker build --no-cache -f ./AdminDashboard/Dockerfile .
docker-build-dashboard:
runs-on: ubuntu-latest
@@ -311,7 +311,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Dashboard/Dockerfile .
command: sudo docker build --no-cache -f ./Dashboard/Dockerfile .
docker-build-probe:
runs-on: ubuntu-latest
@@ -334,7 +334,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Probe/Dockerfile .
command: sudo docker build --no-cache -f ./Probe/Dockerfile .
docker-build-probe-ingest:
runs-on: ubuntu-latest
@@ -357,7 +357,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./ProbeIngest/Dockerfile .
command: sudo docker build --no-cache -f ./ProbeIngest/Dockerfile .
docker-build-server-monitor-ingest:
runs-on: ubuntu-latest
@@ -380,9 +380,9 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
command: sudo docker build --no-cache -f ./ServerMonitorIngest/Dockerfile .
docker-build-open-telemetry-ingest:
docker-build-telemetry:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
@@ -403,7 +403,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
command: sudo docker build --no-cache -f ./Telemetry/Dockerfile .
docker-build-incoming-request-ingest:
runs-on: ubuntu-latest
@@ -426,30 +426,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
docker-build-fluent-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./FluentIngest/Dockerfile .
command: sudo docker build --no-cache -f ./IncomingRequestIngest/Dockerfile .
docker-build-status-page:
runs-on: ubuntu-latest
@@ -472,7 +449,7 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./StatusPage/Dockerfile .
command: sudo docker build --no-cache -f ./StatusPage/Dockerfile .
docker-build-test-server:
runs-on: ubuntu-latest
@@ -495,4 +472,4 @@ jobs:
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./TestServer/Dockerfile .
command: sudo docker build --no-cache -f ./TestServer/Dockerfile .

View File

@@ -319,7 +319,7 @@ jobs:
max_attempts: 3
command: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
compile-open-telemetry-ingest:
compile-telemetry:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
@@ -329,12 +329,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Open Telemetry Ingest
- name: Compile Telemetry
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
command: cd Telemetry && npm install && npm run compile && npm run dep-check
compile-incoming-request-ingest:
@@ -354,24 +354,6 @@ jobs:
max_attempts: 3
command: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
compile-fluent-ingest:
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
- name: Compile Fluent Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd FluentIngest && npm install && npm run compile && npm run dep-check
compile-status-page:
runs-on: ubuntu-latest
env:

49
.github/workflows/npm-audit-fix.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: NPM Audit Fix
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
npm-audit-fix:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run npm audit fix across packages
run: npm run audit-fix
- name: Detect changes
id: changes
run: |
if git status --porcelain | grep .; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
- name: Create pull request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: npm audit fix"
title: "chore: npm audit fix"
body: |
Automated npm audit fix run.
Workflow: ${{ github.workflow }}
Run ID: ${{ github.run_id }}
branch: chore/npm-audit-fix
delete-branch: true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
name: Fluent Ingest 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 FluentIngest && npm install && npm run test

View File

@@ -1,4 +1,4 @@
name: OpenTelemetryIngest Test
name: Telemetry Test
on:
pull_request:
@@ -17,5 +17,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd OpenTelemetryIngest && npm install && npm run test
- run: cd Common && npm install
- run: cd Telemetry && npm install && npm run test

18
.vscode/launch.json vendored
View File

@@ -205,8 +205,8 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/OpenTelemetryIngest",
"name": "OpenTelemetryIngest: Debug with Docker",
"localRoot": "${workspaceFolder}/Telemetry",
"name": "Telemetry: Debug with Docker",
"port": 9938,
"remoteRoot": "/usr/src/app",
"request": "attach",
@@ -217,20 +217,6 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/FluentIngest",
"name": "Fluent Ingest: Debug with Docker",
"port": 9937,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/IsolatedVM",

View File

@@ -14,9 +14,12 @@ RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ARG IS_ENTERPRISE_EDITION=false
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -31,7 +31,7 @@
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.52.1",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
@@ -49,7 +49,9 @@
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
"@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2",
"@types/qrcode": "^1.5.5",
"@types/react-highlight": "^0.12.8",
@@ -58,8 +60,10 @@
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"archiver": "^7.0.1",
"axios": "^1.12.0",
"botbuilder": "^4.23.3",
"bullmq": "^5.61.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
@@ -80,11 +84,11 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.10",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -99,7 +103,7 @@
"react-router-dom": "^6.24.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^16.0.0",
"react-toggle": "^4.1.3",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
@@ -114,13 +118,13 @@
"tailwind-merge": "^2.5.2",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm": "^0.3.26",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -243,89 +247,20 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
@@ -501,19 +436,21 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -528,109 +465,28 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz",
"integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.6",
"@babel/types": "^7.23.6"
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -801,14 +657,15 @@
}
},
"node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -836,14 +693,14 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1760,21 +1617,23 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -2004,10 +1863,11 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -2076,9 +1936,10 @@
}
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
@@ -2225,9 +2086,10 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2244,10 +2106,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -2542,6 +2405,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -3239,7 +3103,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.1",
@@ -3398,12 +3263,13 @@
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -3684,10 +3550,11 @@
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -4107,20 +3974,12 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},

View File

@@ -108,7 +108,7 @@
</div>
</div>
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<%- include('../partials/code', {title: "Example Pagination Request", requestUrl: "/api/monitors/get-list?skip=0&limit=3", requestType: "GET", code: pageData.requestCode }) -%>
<%- include('../partials/code', {title: "Example Pagination Request", requestUrl: "/api/monitors/get-list?skip=0&limit=3", requestType: "POST", code: pageData.requestCode }) -%>
<%- include('../partials/code', {title: "Example Pagination Response" , requestUrl: "", requestType: "", code: pageData.responseCode }) -%>
</div>
</div>

View File

@@ -14,9 +14,12 @@ RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ARG IS_ENTERPRISE_EDITION=false
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -12,7 +12,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"react-router-dom": "^6.30.1",
"use-async-effect": "^2.2.7"
},
"devDependencies": {
@@ -35,7 +35,7 @@
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.52.1",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
@@ -53,7 +53,9 @@
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
"@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2",
"@types/qrcode": "^1.5.5",
"@types/react-highlight": "^0.12.8",
@@ -62,8 +64,10 @@
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"archiver": "^7.0.1",
"axios": "^1.12.0",
"botbuilder": "^4.23.3",
"bullmq": "^5.61.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
@@ -84,11 +88,11 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.10",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -103,7 +107,7 @@
"react-router-dom": "^6.24.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^16.0.0",
"react-toggle": "^4.1.3",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
@@ -118,13 +122,13 @@
"tailwind-merge": "^2.5.2",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm": "^0.3.26",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -344,9 +348,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
"integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -522,21 +526,23 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -683,9 +689,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -704,10 +710,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -794,6 +801,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -958,12 +966,12 @@
}
},
"node_modules/react-router": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz",
"integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.16.1"
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -973,13 +981,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz",
"integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.16.1",
"react-router": "6.23.1"
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
@@ -1009,6 +1017,16 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/simple-update-notifier": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
@@ -1021,15 +1039,6 @@
"node": ">=8.10.0"
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1047,6 +1056,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},

View File

@@ -33,7 +33,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"react-router-dom": "^6.30.1",
"use-async-effect": "^2.2.7"
},
"devDependencies": {

View File

@@ -12,6 +12,7 @@ import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchem
import Link from "Common/UI/Components/Link/Link";
import { DASHBOARD_URL } from "Common/UI/Config";
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
import EditionLabel from "Common/UI/Components/EditionLabel/EditionLabel";
import UiAnalytics from "Common/UI/Utils/Analytics";
import LoginUtil from "Common/UI/Utils/Login";
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
@@ -192,6 +193,9 @@ const LoginPage: () => JSX.Element = () => {
src={OneUptimeLogo}
alt="OneUptime"
/>
<div className="mt-4 flex justify-center">
<EditionLabel />
</div>
{!showTwoFactorAuth && (
<>
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">

View File

@@ -14,9 +14,12 @@ RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ARG IS_ENTERPRISE_EDITION=false
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -1,13 +1,74 @@
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import Express, {
ExpressApplication,
ExpressRequest,
ExpressResponse,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Response from "Common/Server/Utils/Response";
import UserMiddleware from "Common/Server/Middleware/UserAuthorization";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import NotAuthorizedException from "Common/Types/Exception/NotAuthorizedException";
import { JSONObject } from "Common/Types/JSON";
import "ejs";
import JSONWebTokenData from "Common/Types/JsonWebTokenData";
export const APP_NAME: string = "admin";
const app: ExpressApplication = Express.getExpressApp();
type EnsureMasterAdminAccessFunction = (
req: ExpressRequest,
res: ExpressResponse,
) => Promise<JSONObject>;
const ensureMasterAdminAccess: EnsureMasterAdminAccessFunction = async (
req: ExpressRequest,
res: ExpressResponse,
): Promise<JSONObject> => {
try {
const accessToken: string | undefined =
UserMiddleware.getAccessTokenFromExpressRequest(req);
if (!accessToken) {
Response.sendErrorResponse(
req,
res,
new NotAuthorizedException(
"Unauthorized: Only master admins can access the admin dashboard.",
),
);
return {};
}
const authData: JSONWebTokenData = JSONWebToken.decode(accessToken);
if (!authData.isMasterAdmin) {
Response.sendErrorResponse(
req,
res,
new NotAuthorizedException(
"Unauthorized: Only master admins can access the admin dashboard.",
),
);
return {};
}
return {};
} catch (error) {
logger.error(error);
Response.sendErrorResponse(
req,
res,
new NotAuthorizedException(
"Unauthorized: Only master admins can access the admin dashboard.",
),
);
return {};
}
};
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
// init the app
@@ -19,6 +80,7 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
liveCheck: async () => {},
readyCheck: async () => {},
},
getVariablesToRenderIndexPage: ensureMasterAdminAccess,
});
// add default routes

View File

@@ -12,7 +12,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
"react-router-dom": "^6.30.1"
},
"devDependencies": {
"@types/node": "^16.11.35",
@@ -34,7 +34,7 @@
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.52.1",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
@@ -52,7 +52,9 @@
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
"@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2",
"@types/qrcode": "^1.5.5",
"@types/react-highlight": "^0.12.8",
@@ -61,8 +63,10 @@
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"archiver": "^7.0.1",
"axios": "^1.12.0",
"botbuilder": "^4.23.3",
"bullmq": "^5.61.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
@@ -83,11 +87,11 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.10",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -102,7 +106,7 @@
"react-router-dom": "^6.24.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^16.0.0",
"react-toggle": "^4.1.3",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
@@ -117,13 +121,13 @@
"tailwind-merge": "^2.5.2",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm": "^0.3.26",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -343,9 +347,9 @@
"dev": true
},
"node_modules/@remix-run/router": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
"integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -491,21 +495,23 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -601,9 +607,10 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -620,10 +627,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -710,6 +718,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -937,12 +946,12 @@
}
},
"node_modules/react-router": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz",
"integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.16.1"
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -952,13 +961,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz",
"integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.16.1",
"react-router": "6.23.1"
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
@@ -988,6 +997,16 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/simple-update-notifier": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
@@ -1000,15 +1019,6 @@
"node": ">=8.10.0"
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1026,6 +1036,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},

View File

@@ -8,7 +8,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
"react-router-dom": "^6.30.1"
},
"scripts": {
"dev-build": "NODE_ENV=development node esbuild.config.js",

View File

@@ -5,6 +5,7 @@ import Projects from "./Pages/Projects/Index";
import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
// Settings Pages.
import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from "./Pages/Settings/Probes/Index";
@@ -25,6 +26,7 @@ import {
} from "react-router-dom";
import UserView from "./Pages/Users/View/Index";
import UserDelete from "./Pages/Users/View/Delete";
import UserSettings from "./Pages/Users/View/Settings";
import ProjectView from "./Pages/Projects/View/Index";
import ProjectDelete from "./Pages/Projects/View/Delete";
@@ -70,6 +72,11 @@ const App: () => JSX.Element = () => {
element={<UserView />}
/>
<PageRoute
path={RouteMap[PageMap.USER_SETTINGS]?.toString() || ""}
element={<UserSettings />}
/>
<PageRoute
path={RouteMap[PageMap.USER_DELETE]?.toString() || ""}
element={<UserDelete />}
@@ -105,6 +112,11 @@ const App: () => JSX.Element = () => {
element={<SettingsCallSMS />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_WHATSAPP]?.toString() || ""}
element={<SettingsWhatsApp />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ""}
element={<SettingsProbes />}

View File

@@ -3,6 +3,7 @@ import Logo from "./Logo";
import UserProfile from "./UserProfile";
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
import Header from "Common/UI/Components/Header/Header";
import EditionLabel from "Common/UI/Components/EditionLabel/EditionLabel";
import { DASHBOARD_URL } from "Common/UI/Config";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
@@ -27,6 +28,7 @@ const DashboardHeader: FunctionComponent = (): ReactElement => {
}
rightComponents={
<>
<EditionLabel className="mr-3 hidden md:inline-flex" />
<Button
title="Exit Admin"
buttonStyle={ButtonStyleType.NORMAL}

View File

@@ -50,6 +50,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "WhatsApp",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
),
}}
icon={IconProp.WhatsApp}
/>
</SideMenuSection>
<SideMenuSection title="Monitoring">

View File

@@ -0,0 +1,491 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from "../SideMenu";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import Page from "Common/UI/Components/Page/Page";
import FieldType from "Common/UI/Components/Types/FieldType";
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
import React, { FunctionComponent, ReactElement, useState } from "react";
import Card from "Common/UI/Components/Card/Card";
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
import BasicForm from "Common/UI/Components/Forms/BasicForm";
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
import { JSONObject } from "Common/Types/JSON";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import API from "Common/UI/Utils/API/API";
import { APP_API_URL } from "Common/UI/Config";
import WhatsAppTemplateMessages, {
WhatsAppTemplateId,
WhatsAppTemplateIds,
WhatsAppTemplateLanguage,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
type ToFriendlyName = (value: string) => string;
const toFriendlyName: ToFriendlyName = (value: string): string => {
return value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/_/g, " ")
.replace(/\s+/g, " ")
.trim();
};
type ExtractTemplateVariables = (template: string) => Array<string>;
const extractTemplateVariables: ExtractTemplateVariables = (
template: string,
): Array<string> => {
const matches: RegExpMatchArray | null = template.match(/\{\{(.*?)\}\}/g);
if (!matches) {
return [];
}
const uniqueVariables: Set<string> = new Set<string>();
for (const match of matches) {
const variable: string = match.replace("{{", "").replace("}}", "").trim();
if (variable) {
uniqueVariables.add(variable);
}
}
return Array.from(uniqueVariables).sort((a: string, b: string) => {
return a.localeCompare(b);
});
};
type BuildWhatsAppSetupMarkdown = () => string;
const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
const templateKeys: Array<keyof typeof WhatsAppTemplateIds> = Object.keys(
WhatsAppTemplateIds,
) as Array<keyof typeof WhatsAppTemplateIds>;
const appApiBaseUrl: string = APP_API_URL.toString().replace(/\/$/, "");
const primaryWebhookUrl: string = `${appApiBaseUrl}/notification/whatsapp/webhook`;
const description: string =
"Follow these steps to connect Meta WhatsApp with OneUptime so notifications can be delivered via WhatsApp.";
const prerequisitesList: Array<string> = [
"Meta Business Manager admin access for the WhatsApp Business Account.",
"A WhatsApp Business phone number approved for API messaging.",
"Admin access to OneUptime with permission to edit global notification settings.",
"A webhook verify token string that you'll configure identically in Meta and OneUptime.",
];
const setupStepsList: Array<string> = [
"Sign in to the [Meta Business Manager](https://business.facebook.com/) with admin access to your WhatsApp Business Account.",
"From **Business Settings → Accounts → WhatsApp Accounts**, create or select the account that owns your sender phone number.",
"In Buisness Portfolio, create a system user and assign it to the WhatsApp Business Account with the role of **Admin**.",
"Generate a token for this system user and this will be your long-lived access token. Make sure to select the **whatsapp_business_management** and **whatsapp_business_messaging** permissions when generating the token.",
"Paste the access token, phone number ID, and webhook verify token into the **Meta WhatsApp Settings** card above, then save.",
"For the **Business Account ID**, go to **Business Settings → Business Info** (or **Business Settings → WhatsApp Accounts → Settings**) and copy the **WhatsApp Business Account ID** value.",
"To locate the **App ID** and **App Secret**, open [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app, then navigate to **Settings → Basic**. The App ID is shown at the top; click **Show** next to **App Secret** to reveal and copy it.",
"Create each template listed below in the Meta WhatsApp Manager. Make sure the template name, language, and variables match exactly. You can however change the content to your preference. Please make sure it's approved by Meta.",
"Send a test notification from OneUptime to confirm that WhatsApp delivery succeeds.",
];
const prerequisitesMarkdown: string = prerequisitesList
.map((item: string) => {
return `- ${item}`;
})
.join("\n");
const setupStepsMarkdown: string = setupStepsList
.map((item: string, index: number) => {
return `${index + 1}. ${item}`;
})
.join("\n");
const tableRows: string = templateKeys
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
const friendlyName: string = toFriendlyName(enumKey.toString());
const templateMessage: string = WhatsAppTemplateMessages[templateId];
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
const variables: Array<string> =
extractTemplateVariables(templateMessage);
const variableList: string =
variables.length > 0
? variables
.map((variable: string) => {
return `\`${variable}\``;
})
.join(", ")
: "_None_";
return `| ${friendlyName} | \`${templateId}\` | ${language} | ${variableList} |`;
})
.join("\n");
const templateBodies: string = templateKeys
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
const friendlyName: string = toFriendlyName(enumKey.toString());
const templateMessage: string = WhatsAppTemplateMessages[templateId];
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
const variables: Array<string> =
extractTemplateVariables(templateMessage);
const variableMarkdown: string =
variables.length > 0
? variables
.map((variable: string) => {
return `- \`${variable}\``;
})
.join("\n")
: "_None_";
const variablesHeading: string = variables.length
? `**Variables (${variables.length})**`
: "**Variables**";
return [
`#### ${friendlyName}`,
"",
`**Template Name:** \`${templateId}\``,
`**Language:** ${language}`,
"",
variablesHeading,
variableMarkdown,
"",
"**Body**",
"```plaintext",
templateMessage,
"```",
"",
"---",
].join("\n");
})
.join("\n\n");
const templateSummaryTable: string = [
"| Friendly Name | Template Name | Language | Variables |",
"| --- | --- | --- | --- |",
tableRows,
]
.filter(Boolean)
.join("\n");
const webhookSection: string = [
"### Configure Meta Webhook Subscription",
"1. In the OneUptime Admin Dashboard, open **Settings → WhatsApp → Meta WhatsApp Settings** and enter a strong value in **Webhook Verify Token**. Save the form so the encrypted token is stored in Global Config.",
"2. Keep that verify token handy—Meta does not generate one for you. You'll paste the exact same value when configuring the callback.",
"3. In [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app and navigate to **WhatsApp → Configuration → Webhooks**.",
`4. Click **Configure**, then supply one of the following callback URLs when Meta asks for your endpoint:\n - \`${primaryWebhookUrl}\`\n `,
"5. Paste the verify token from step 1 into Meta's **Verify Token** field and submit. Meta will call the callback URL and expect that value to match before it approves the subscription.",
"6. After verification succeeds, subscribe to the **messages** field (and any other WhatsApp webhook categories you need) so delivery status updates are forwarded to OneUptime.",
]
.filter(Boolean)
.join("\n\n");
return [
description,
"### Prerequisites",
prerequisitesMarkdown,
"### Setup Steps",
setupStepsMarkdown,
webhookSection,
"### Required WhatsApp Templates",
templateSummaryTable,
"### Template Bodies",
"> Copy the exact template body below—including punctuation and spacing—when creating each template inside Meta. The variables list shows every placeholder that must be configured in WhatsApp Manager.",
templateBodies,
]
.filter(Boolean)
.join("\n\n");
};
const whatsappSetupMarkdown: string = buildWhatsAppSetupMarkdown();
const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
const [testError, setTestError] = useState<string>("");
const [testSuccess, setTestSuccess] = useState<string>("");
return (
<Page
title={"Admin Settings"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route,
),
},
{
title: "WhatsApp",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
),
},
]}
sideMenu={<DashboardSideMenu />}
>
<CardModelDetail
name="Meta WhatsApp Settings"
cardProps={{
title: "Meta WhatsApp Settings",
description:
"Configure Meta WhatsApp credentials. These values are used to send WhatsApp notifications from OneUptime.",
}}
isEditable={true}
editButtonText="Edit Meta WhatsApp Config"
formSteps={[
{
title: "Credentials",
id: "meta-credentials",
},
{
title: "Meta App",
id: "meta-app",
},
]}
formFields={[
{
field: {
metaWhatsAppAccessToken: true,
},
title: "Access Token",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.EncryptedText,
required: true,
description:
"Long-lived access token generated in the Meta WhatsApp Business Platform.",
placeholder: "EAAG...",
},
{
field: {
metaWhatsAppPhoneNumberId: true,
},
title: "Phone Number ID",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.Text,
required: true,
description:
"The WhatsApp Business phone number ID associated with your sending number.",
placeholder: "123456789012345",
},
{
field: {
metaWhatsAppBusinessAccountId: true,
},
title: "Business Account ID",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.Text,
required: false,
description:
"Optional Business Account ID that owns the WhatsApp templates.",
placeholder: "123456789012345",
},
{
field: {
metaWhatsAppWebhookVerifyToken: true,
},
title: "Webhook Verify Token",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
description:
"Secret token configured in Meta to validate webhook subscription requests.",
placeholder: "Webhook verify token",
},
{
field: {
metaWhatsAppAppId: true,
},
title: "App ID",
stepId: "meta-app",
fieldType: FormFieldSchemaType.Text,
required: false,
description:
"Optional Facebook App ID tied to your WhatsApp integration.",
placeholder: "987654321098765",
},
{
field: {
metaWhatsAppAppSecret: true,
},
title: "App Secret",
stepId: "meta-app",
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
description:
"Optional Facebook App Secret used for webhook signature verification.",
placeholder: "Facebook App Secret",
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config-meta-whatsapp",
fields: [
{
field: {
metaWhatsAppAccessToken: true,
},
title: "Access Token",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppPhoneNumberId: true,
},
title: "Phone Number ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppBusinessAccountId: true,
},
title: "Business Account ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppWebhookVerifyToken: true,
},
title: "Webhook Verify Token",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppAppId: true,
},
title: "App ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppAppSecret: true,
},
title: "App Secret",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
<Card
title="Send Test WhatsApp Message"
description="Send a test WhatsApp template message to confirm your Meta configuration."
>
{testSuccess ? (
<Alert
type={AlertType.SUCCESS}
title={testSuccess}
className="mb-4"
/>
) : (
<></>
)}
<BasicForm
id="send-test-whatsapp-form"
name="Send Test WhatsApp Message"
isLoading={isSendingTest}
error={testError || ""}
submitButtonText="Send Test Message"
maxPrimaryButtonWidth={true}
initialValues={{
phoneNumber: "",
}}
fields={[
{
field: {
phoneNumber: true,
},
title: "Recipient WhatsApp Number",
description:
"Enter the full international phone number (including country code) that should receive the test message.",
placeholder: "+11234567890",
required: true,
fieldType: FormFieldSchemaType.Phone,
disableSpellCheck: true,
},
]}
onSubmit={async (
values: JSONObject,
onSubmitSuccessful?: () => void,
) => {
const toPhone: string = String(values["phoneNumber"] || "").trim();
if (!toPhone) {
setTestSuccess("");
setTestError(
"Please enter a WhatsApp number before sending a test message.",
);
return;
}
setIsSendingTest(true);
setTestError("");
setTestSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/whatsapp/test",
),
data: {
toPhone,
templateKey: WhatsAppTemplateIds.TestNotification,
templateLanguageCode:
WhatsAppTemplateLanguage[
WhatsAppTemplateIds.TestNotification
],
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send test WhatsApp message.");
}
setTestSuccess(
"Test WhatsApp message sent successfully. Check the recipient device to confirm delivery.",
);
if (onSubmitSuccessful) {
onSubmitSuccessful();
}
} catch (err) {
setTestError(API.getFriendlyMessage(err));
} finally {
setIsSendingTest(false);
}
}}
/>
</Card>
<Card
title="Meta WhatsApp Setup Guide"
description="Steps to connect Meta WhatsApp and the templates you must provision."
>
<MarkdownViewer text={whatsappSetupMarkdown} />
</Card>
</Page>
);
};
export default SettingsWhatsApp;

View File

@@ -0,0 +1,94 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import SideMenuComponent from "./SideMenu";
import User from "Common/Models/DatabaseModels/User";
import ModelPage from "Common/UI/Components/Page/ModelPage";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import FieldType from "Common/UI/Components/Types/FieldType";
const UserSettings: FunctionComponent = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<ModelPage<User>
modelId={modelId}
modelNameField="email"
modelType={User}
title={"User"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Users",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
},
{
title: "User",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_VIEW] as Route,
{
modelId: modelId,
},
),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_SETTINGS] as Route,
{
modelId: modelId,
},
),
},
]}
sideMenu={<SideMenuComponent modelId={modelId} />}
>
<CardModelDetail<User>
name="user-master-admin-settings"
cardProps={{
title: "Master Admin Access",
description:
"Grant or revoke master admin access for this user. Master admins can manage every project and workspace.",
}}
isEditable={true}
editButtonText="Update Access"
formFields={[
{
field: {
isMasterAdmin: true,
},
title: "Master Admin",
description:
"Enable to give this user full access to the entire platform.",
fieldType: FormFieldSchemaType.Toggle,
required: true,
},
]}
modelDetailProps={{
modelType: User,
id: "user-master-admin-settings-detail",
fields: [
{
field: {
isMasterAdmin: true,
},
title: "Master Admin",
fieldType: FieldType.Boolean,
placeholder: "No",
},
],
modelId: modelId,
}}
/>
</ModelPage>
);
};
export default UserSettings;

View File

@@ -30,6 +30,18 @@ const SideMenuComponent: FunctionComponent<SideMenuProps> = (
}}
icon={IconProp.Info}
/>
<SideMenuItem
link={{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.USER_SETTINGS] as Route,
{
modelId: props.modelId,
},
),
}}
icon={IconProp.Settings}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">

View File

@@ -6,6 +6,7 @@ enum PageMap {
USERS = "USERS",
USER_VIEW = "USER_VIEW",
USER_SETTINGS = "USER_SETTINGS",
USER_DELETE = "USER_DELETE",
PROJECTS = "PROJECTS",
@@ -15,6 +16,7 @@ enum PageMap {
SETTINGS_HOST = "SETTINGS_HOST",
SETTINGS_SMTP = "SETTINGS_SMTP",
SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
SETTINGS_WHATSAPP = "SETTINGS_WHATSAPP",
SETTINGS_PROBES = "SETTINGS_PROBES",
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = "SETTINGS_API_KEY",

View File

@@ -18,6 +18,9 @@ const RouteMap: Dictionary<Route> = {
[PageMap.USERS]: new Route(`/admin/users`),
[PageMap.USER_VIEW]: new Route(`/admin/users/${RouteParams.ModelID}`),
[PageMap.USER_SETTINGS]: new Route(
`/admin/users/${RouteParams.ModelID}/settings`,
),
[PageMap.USER_DELETE]: new Route(
`/admin/users/${RouteParams.ModelID}/delete`,
),
@@ -25,6 +28,7 @@ const RouteMap: Dictionary<Route> = {
[PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`),
[PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`),
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
[PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`),
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
[PageMap.SETTINGS_AUTHENTICATION]: new Route(
`/admin/settings/authentication`,

View File

@@ -14,9 +14,12 @@ RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ARG IS_ENTERPRISE_EDITION=false
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -14,9 +14,11 @@ import TelemetryAPI from "Common/Server/API/TelemetryAPI";
import ProbeAPI from "Common/Server/API/ProbeAPI";
import ProjectAPI from "Common/Server/API/ProjectAPI";
import ProjectSsoAPI from "Common/Server/API/ProjectSSO";
import WhatsAppLogAPI from "./WhatsAppLogAPI";
// Import API
import ResellerPlanAPI from "Common/Server/API/ResellerPlanAPI";
import EnterpriseLicenseAPI from "Common/Server/API/EnterpriseLicenseAPI";
import MonitorAPI from "Common/Server/API/MonitorAPI";
import ShortLinkAPI from "Common/Server/API/ShortLinkAPI";
import StatusPageAPI from "Common/Server/API/StatusPageAPI";
@@ -31,6 +33,7 @@ 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 UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
import UserPushAPI from "Common/Server/API/UserPushAPI";
import ApiKeyPermissionService, {
Service as ApiKeyPermissionServiceType,
@@ -96,6 +99,9 @@ import IncidentInternalNoteService, {
import IncidentNoteTemplateService, {
Service as IncidentNoteTemplateServiceType,
} from "Common/Server/Services/IncidentNoteTemplateService";
import IncidentPostmortemTemplateService, {
Service as IncidentPostmortemTemplateServiceType,
} from "Common/Server/Services/IncidentPostmortemTemplateService";
import TableViewService, {
Service as TableViewServiceType,
} from "Common/Server/Services/TableViewService";
@@ -139,9 +145,6 @@ import LogService, {
LogService as LogServiceType,
} from "Common/Server/Services/LogService";
import TelemetryAttributeService, {
TelemetryAttributeService as TelemetryAttributeServiceType,
} from "Common/Server/Services/TelemetryAttributeService";
import CopilotActionTypePriorityService, {
Service as CopilotActionTypePriorityServiceType,
} from "Common/Server/Services/CopilotActionTypePriorityService";
@@ -378,6 +381,7 @@ import TelemetryExceptionService, {
import ExceptionInstanceService, {
ExceptionInstanceService as ExceptionInstanceServiceType,
} from "Common/Server/Services/ExceptionInstanceService";
import AcmeChallengeAPI from "Common/Server/API/AcmeChallengeAPI";
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
@@ -408,6 +412,7 @@ import Incident from "Common/Models/DatabaseModels/Incident";
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
import IncidentInternalNote from "Common/Models/DatabaseModels/IncidentInternalNote";
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote";
@@ -487,7 +492,6 @@ import WorkflowVariable from "Common/Models/DatabaseModels/WorkflowVariable";
import ProbeOwnerTeam from "Common/Models/DatabaseModels/ProbeOwnerTeam";
import ProbeOwnerUser from "Common/Models/DatabaseModels/ProbeOwnerUser";
import ServiceCatalogDependency from "Common/Models/DatabaseModels/ServiceCatalogDependency";
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";
@@ -613,10 +617,7 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAnalyticsAPI<TelemetryAttribute, TelemetryAttributeServiceType>(
TelemetryAttribute,
TelemetryAttributeService,
).getRouter(),
new AcmeChallengeAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, OpenAPI.getRouter());
@@ -1398,6 +1399,17 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentPostmortemTemplate,
IncidentPostmortemTemplateServiceType
>(
IncidentPostmortemTemplate,
IncidentPostmortemTemplateService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
@@ -1538,6 +1550,11 @@ const BaseAPIFeatureSet: FeatureSet = {
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new WhatsAppLogAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
@@ -1635,6 +1652,10 @@ const BaseAPIFeatureSet: FeatureSet = {
`/${APP_NAME.toLocaleLowerCase()}`,
new ResellerPlanAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new EnterpriseLicenseAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
@@ -1675,6 +1696,10 @@ 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 UserWhatsAppAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());

View File

@@ -0,0 +1,14 @@
import BaseAPI from "Common/Server/API/BaseAPI";
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
import WhatsAppLogService, {
Service as WhatsAppLogServiceType,
} from "Common/Server/Services/WhatsAppLogService";
export default class WhatsAppLogAPI extends BaseAPI<
WhatsAppLog,
WhatsAppLogServiceType
> {
public constructor() {
super(WhatsAppLog, WhatsAppLogService);
}
}

View File

@@ -25,24 +25,70 @@ import EmailVerificationTokenService from "Common/Server/Services/EmailVerificat
import MailService from "Common/Server/Services/MailService";
import UserService from "Common/Server/Services/UserService";
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
import UserSessionService, {
SessionMetadata,
} from "Common/Server/Services/UserSessionService";
import CookieUtil from "Common/Server/Utils/Cookie";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import TotpAuth from "Common/Server/Utils/TotpAuth";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
import User from "Common/Models/DatabaseModels/User";
import UserSession from "Common/Models/DatabaseModels/UserSession";
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
type FinalizeUserLoginInput = {
req: ExpressRequest;
res: ExpressResponse;
user: User;
isGlobalLogin: boolean;
};
const finalizeUserLogin: (
data: FinalizeUserLoginInput,
) => Promise<SessionMetadata> = async (
data: FinalizeUserLoginInput,
): Promise<SessionMetadata> => {
const { req, res, user, isGlobalLogin } = data;
const sessionMetadata: SessionMetadata =
await UserSessionService.createSession({
userId: user.id!,
isGlobalLogin,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
CookieUtil.setUserCookie({
expressResponse: res,
user,
isGlobalLogin,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return sessionMetadata;
};
router.post(
"/signup",
async (
@@ -185,9 +231,9 @@ router.post(
if (savedUser) {
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(savedUser.id!);
CookieUtil.setUserCookie({
expressResponse: res,
await finalizeUserLogin({
req,
res,
user: savedUser,
isGlobalLogin: true,
});
@@ -487,6 +533,127 @@ router.post(
},
);
router.post(
"/refresh-token",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
if (!refreshToken) {
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException(
"Refresh token missing. Please login again.",
),
);
}
const session: UserSession | null =
await UserSessionService.findActiveSessionByRefreshToken(refreshToken);
if (!session || !session.id) {
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (
session.refreshTokenExpiresAt &&
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
) {
await UserSessionService.revokeSessionById(session.id, {
reason: "Refresh token expired",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (!session.userId) {
await UserSessionService.revokeSessionById(session.id, {
reason: "Session missing user",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
const user: User | null = await UserService.findOneById({
id: session.userId,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
name: true,
isMasterAdmin: true,
profilePictureId: true,
timezone: true,
enableTwoFactorAuth: true,
},
});
if (!user) {
await UserSessionService.revokeSessionById(session.id, {
reason: "User not found",
});
CookieUtil.removeAllCookies(req, res);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Account no longer exists."),
);
}
const additionalInfo: JSONObject = (session.additionalInfo ||
{}) as JSONObject;
const isGlobalLogin: boolean =
typeof additionalInfo["isGlobalLogin"] === "boolean"
? (additionalInfo["isGlobalLogin"] as boolean)
: true;
const renewedSession: SessionMetadata =
await UserSessionService.renewSessionWithNewRefreshToken({
session,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
CookieUtil.setUserCookie({
expressResponse: res,
user,
isGlobalLogin,
sessionId: renewedSession.session.id!,
refreshToken: renewedSession.refreshToken,
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/logout",
async (
@@ -495,6 +662,15 @@ router.post(
next: NextFunction,
): Promise<void> => {
try {
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
if (refreshToken) {
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
reason: "User logout",
});
}
CookieUtil.removeAllCookies(req, res);
return Response.sendEmptySuccessResponse(req, res);
@@ -788,8 +964,9 @@ const login: LoginFunction = async (options: {
if (alreadySavedUser.password.toString() === user.password!.toString()) {
logger.info("User logged in: " + alreadySavedUser.email?.toString());
CookieUtil.setUserCookie({
expressResponse: res,
await finalizeUserLogin({
req,
res,
user: alreadySavedUser,
isGlobalLogin: true,
});

View File

@@ -6,6 +6,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
@@ -166,13 +167,7 @@ const formatTeamForSCIM: (
}
return {
// Include both SCIM 2.0 Group schema and SCIM 1.1 core schema for broader compatibility
// Some provisioning agents (e.g., Okta On-Prem) expect 'urn:scim:schemas:core:1.0'
// to be present even when using SCIM v2 endpoints.
schemas: [
"urn:ietf:params:scim:schemas:core:2.0:Group",
"urn:scim:schemas:core:1.0",
],
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
id: team.id?.toString(),
displayName: team.name?.toString(),
members: members,
@@ -189,7 +184,11 @@ const formatTeamForSCIM: (
router.get(
"/scim/v2/:projectScimId/ServiceProviderConfig",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Project SCIM ServiceProviderConfig - scimId: ${req.params["projectScimId"]!}`,
@@ -208,7 +207,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -217,7 +216,11 @@ router.get(
router.get(
"/scim/v2/:projectScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Project SCIM Users list - scimId: ${req.params["projectScimId"]!}`,
@@ -390,7 +393,7 @@ router.get(
);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -399,7 +402,11 @@ router.get(
router.get(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -458,7 +465,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -467,7 +474,11 @@ router.get(
router.put(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -624,7 +635,7 @@ router.put(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -633,7 +644,11 @@ router.put(
router.get(
"/scim/v2/:projectScimId/Groups",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}`,
@@ -713,7 +728,7 @@ router.get(
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -722,7 +737,11 @@ router.get(
router.get(
"/scim/v2/:projectScimId/Groups/:groupId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Get individual group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -777,7 +796,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, group);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -786,7 +805,11 @@ router.get(
router.post(
"/scim/v2/:projectScimId/Groups",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Create group request for projectScimId: ${req.params["projectScimId"]}`,
@@ -811,41 +834,53 @@ router.post(
projectId: projectId,
name: displayName,
},
select: { _id: true },
select: {
_id: true,
name: true,
createdAt: true,
updatedAt: true,
projectId: true,
},
props: { isRoot: true },
});
let targetTeam: Team;
let createdNewTeam: boolean = false;
if (existingTeam) {
logger.debug(
`SCIM Create group - team already exists with id: ${existingTeam.id}`,
`SCIM Create group - team already exists with id: ${existingTeam.id}, reusing existing team`,
);
throw new BadRequestException("Group with this name already exists");
targetTeam = existingTeam;
} else {
// Create new team
logger.debug(`SCIM Create group - creating new team: ${displayName}`);
const team: Team = new Team();
team.projectId = projectId;
team.name = displayName;
team.isTeamEditable = true; // Allow editing SCIM-created teams
team.isTeamDeleteable = true; // Allow deleting SCIM-created teams
team.shouldHaveAtLeastOneMember = false; // SCIM groups can be empty
const createdTeam: Team = await TeamService.create({
data: team,
props: { isRoot: true },
});
logger.debug(
`SCIM Create group - created team with id: ${createdTeam.id}`,
);
targetTeam = createdTeam;
createdNewTeam = true;
}
// Create new team
logger.debug(`SCIM Create group - creating new team: ${displayName}`);
const team: Team = new Team();
team.projectId = projectId;
team.name = displayName;
team.isTeamEditable = true; // Allow editing SCIM-created teams
team.isTeamDeleteable = true; // Allow deleting SCIM-created teams
team.shouldHaveAtLeastOneMember = false; // SCIM groups can be empty
const createdTeam: Team = await TeamService.create({
data: team,
props: { isRoot: true },
});
logger.debug(
`SCIM Create group - created team with id: ${createdTeam.id}`,
);
// Handle initial members if provided
// Handle members if provided. Adds any new members and leaves existing ones intact.
const members: Array<SCIMMember> =
(scimGroup["members"] as Array<SCIMMember>) || [];
if (members.length > 0) {
logger.debug(
`SCIM Create group - adding ${members.length} initial members`,
`SCIM Create group - ensuring ${members.length} members are part of team ${targetTeam.id}`,
);
for (const member of members) {
const userId: string = member["value"] as string;
@@ -864,18 +899,18 @@ router.post(
query: {
projectId: projectId,
userId: new ObjectID(userId),
teamId: createdTeam.id!,
teamId: targetTeam.id!,
},
select: { _id: true },
props: { isRoot: true },
});
if (!existingMember) {
// Add user to the new team
// Add user to the team
const newTeamMember: TeamMember = new TeamMember();
newTeamMember.projectId = projectId;
newTeamMember.userId = new ObjectID(userId);
newTeamMember.teamId = createdTeam.id!;
newTeamMember.teamId = targetTeam.id!;
newTeamMember.hasAcceptedInvitation = true;
newTeamMember.invitationAcceptedAt =
OneUptimeDate.getCurrentDate();
@@ -887,7 +922,7 @@ router.post(
},
});
logger.debug(
`SCIM Create group - added user ${userId} to team`,
`SCIM Create group - added user ${userId} to team ${targetTeam.id}`,
);
}
}
@@ -895,21 +930,42 @@ router.post(
}
}
const createdGroup: JSONObject = await formatTeamForSCIM(
createdTeam,
const teamForResponse: Team | null = await TeamService.findOneById({
id: targetTeam.id!,
select: {
_id: true,
name: true,
createdAt: true,
updatedAt: true,
projectId: true,
},
props: { isRoot: true },
});
if (!teamForResponse) {
throw new NotFoundException("Failed to retrieve group");
}
const groupResponse: JSONObject = await formatTeamForSCIM(
teamForResponse,
req.params["projectScimId"]!,
true,
);
logger.debug(
`SCIM Create group - returning created group with id: ${createdTeam.id}`,
`SCIM Create group - returning group with id: ${teamForResponse.id}`,
);
res.status(201);
return Response.sendJsonObjectResponse(req, res, createdGroup);
if (createdNewTeam) {
res.status(201);
} else {
res.status(200);
}
return Response.sendJsonObjectResponse(req, res, groupResponse);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -918,7 +974,11 @@ router.post(
router.put(
"/scim/v2/:projectScimId/Groups/:groupId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Update group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -968,10 +1028,6 @@ router.put(
);
}
if (!team.isTeamEditable) {
throw new BadRequestException("This group cannot be updated");
}
// Update team name if provided
const displayName: string = scimGroup["displayName"] as string;
if (displayName && displayName !== team.name) {
@@ -1072,7 +1128,7 @@ router.put(
throw new NotFoundException("Failed to retrieve updated group");
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -1081,7 +1137,11 @@ router.put(
router.delete(
"/scim/v2/:projectScimId/Groups/:groupId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Delete group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -1159,7 +1219,7 @@ router.delete(
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -1168,7 +1228,11 @@ router.delete(
router.patch(
"/scim/v2/:projectScimId/Groups/:groupId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Patch group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -1214,10 +1278,6 @@ router.patch(
);
}
if (!team.isTeamEditable) {
throw new BadRequestException("This group cannot be updated");
}
// Handle SCIM patch operations
const operations: JSONObject[] =
(scimPatch["Operations"] as JSONObject[]) || [];
@@ -1403,7 +1463,7 @@ router.patch(
throw new NotFoundException("Failed to retrieve updated group");
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -1412,7 +1472,11 @@ router.patch(
router.post(
"/scim/v2/:projectScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Create user request for projectScimId: ${req.params["projectScimId"]}`,
@@ -1499,7 +1563,7 @@ router.post(
return Response.sendJsonObjectResponse(req, res, createdUser);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -1508,7 +1572,11 @@ router.post(
router.delete(
"/scim/v2/:projectScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
@@ -1560,7 +1628,7 @@ router.delete(
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);

View File

@@ -20,6 +20,9 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
import ProjectSSOService from "Common/Server/Services/ProjectSsoService";
import TeamMemberService from "Common/Server/Services/TeamMemberService";
import UserService from "Common/Server/Services/UserService";
import UserSessionService, {
SessionMetadata,
} from "Common/Server/Services/UserSessionService";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
import Select from "Common/Server/Types/Database/Select";
import CookieUtil from "Common/Server/Utils/Cookie";
@@ -28,6 +31,9 @@ import Express, {
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -40,6 +46,8 @@ import Name from "Common/Types/Name";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
/*
* This route is used to get the SSO config for the user.
* when the user logs in from OneUptime and not from the IDP.
@@ -47,7 +55,11 @@ const router: ExpressRouter = Express.getRouter();
router.get(
"/service-provider-login",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
if (!req.query["email"]) {
return Response.sendErrorResponse(
@@ -152,7 +164,11 @@ router.get(
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, err as Exception);
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);
@@ -162,7 +178,7 @@ router.get(
async (
req: ExpressRequest,
res: ExpressResponse,
_next: NextFunction,
next: NextFunction,
): Promise<void> => {
try {
if (!req.params["projectId"]) {
@@ -238,22 +254,42 @@ router.get(
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, err as Exception);
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);
router.get(
"/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res);
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
await loginUserWithSso(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res);
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
await loginUserWithSso(req, res);
} catch (err) {
return next(err);
}
},
);
@@ -511,15 +547,31 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
expressResponse: res,
});
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
const sessionMetadata: SessionMetadata =
await UserSessionService.createSession({
userId: alreadySavedUser.id!,
isGlobalLogin: false,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
additionalInfo: {
projectId: projectId.toString(),
},
});
CookieUtil.setUserCookie({
expressResponse: res,
user: alreadySavedUser,
isGlobalLogin: false,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

View File

@@ -6,30 +6,93 @@ import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import BadDataException from "Common/Types/Exception/BadDataException";
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from "Common/Server/DatabaseConfig";
import { EncryptionSecret } from "Common/Server/EnvironmentConfig";
import MailService from "Common/Server/Services/MailService";
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
import StatusPageService from "Common/Server/Services/StatusPageService";
import StatusPagePrivateUserSessionService, {
SessionMetadata as StatusPageSessionMetadata,
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
import CookieUtil from "Common/Server/Utils/Cookie";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
import StatusPagePrivateUserSession from "Common/Models/DatabaseModels/StatusPagePrivateUserSession";
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
type FinalizeStatusPageLoginInput = {
req: ExpressRequest;
res: ExpressResponse;
user: StatusPagePrivateUser;
};
const finalizeStatusPageLogin: (data: FinalizeStatusPageLoginInput) => Promise<{
sessionMetadata: StatusPageSessionMetadata;
accessToken: string;
}> = async (
data: FinalizeStatusPageLoginInput,
): Promise<{
sessionMetadata: StatusPageSessionMetadata;
accessToken: string;
}> => {
const { req, res, user } = data;
if (!user.projectId) {
throw new BadDataException(
"Status page user is missing associated projectId.",
);
}
if (!user.statusPageId) {
throw new BadDataException(
"Status page user is missing associated statusPageId.",
);
}
const sessionMetadata: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.createSession({
projectId: user.projectId,
statusPageId: user.statusPageId,
statusPagePrivateUserId: user.id!,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user,
statusPageId: user.statusPageId,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return {
sessionMetadata,
accessToken,
};
};
router.post(
"/logout/:statuspageid",
async (
@@ -46,7 +109,20 @@ router.post(
req.params["statuspageid"].toString(),
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId)); // remove the cookie.
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
if (refreshToken) {
await StatusPagePrivateUserSessionService.revokeSessionByRefreshToken(
refreshToken,
{
reason: "User logged out",
},
);
}
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(res, CookieUtil.getRefreshTokenKey(statusPageId));
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
@@ -55,6 +131,198 @@ router.post(
},
);
router.post(
"/refresh-token/:statuspageid",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const statusPageIdParam: string | undefined = req.params["statuspageid"];
if (!statusPageIdParam) {
throw new BadDataException("Status Page ID is required.");
}
const statusPageId: ObjectID = new ObjectID(statusPageIdParam.toString());
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
if (!refreshToken) {
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException(
"Refresh token missing. Please login again.",
),
);
}
const session: StatusPagePrivateUserSession | null =
await StatusPagePrivateUserSessionService.findActiveSessionByRefreshToken(
refreshToken,
);
if (!session || !session.id || !session.statusPageId) {
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (session.statusPageId.toString() !== statusPageId.toString()) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Status page mismatch",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (
session.refreshTokenExpiresAt &&
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Refresh token expired",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
if (!session.statusPagePrivateUserId) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "Session missing user",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Session expired. Please login again."),
);
}
const user: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: session.statusPagePrivateUserId,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
});
if (!user) {
await StatusPagePrivateUserSessionService.revokeSessionById(
session.id,
{
reason: "User not found",
},
);
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
CookieUtil.removeCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
);
return Response.sendErrorResponse(
req,
res,
new NotAuthenticatedException("Account no longer exists."),
);
}
const renewedSession: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.renewSessionWithNewRefreshToken(
{
session,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
},
);
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user,
statusPageId: user.statusPageId!,
sessionId: renewedSession.session.id!,
refreshToken: renewedSession.refreshToken,
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendEntityResponse(
req,
res,
user,
StatusPagePrivateUser,
{
miscData: {
token: accessToken,
},
},
);
} catch (err) {
return next(err);
}
},
);
router.post(
"/forgot-password",
async (
@@ -376,6 +644,7 @@ router.post(
password: true,
email: true,
statusPageId: true,
projectId: true,
},
props: {
isRoot: true,
@@ -383,31 +652,38 @@ router.post(
});
if (alreadySavedUser) {
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30),
),
const { accessToken } = await finalizeStatusPageLogin({
req,
res,
user: alreadySavedUser,
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
},
);
const sanitizedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneById({
id: alreadySavedUser.id!,
props: {
isRoot: true,
},
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
});
if (!sanitizedUser && (alreadySavedUser as any).password) {
delete (alreadySavedUser as any).password;
}
return Response.sendEntityResponse(
req,
res,
alreadySavedUser,
sanitizedUser || alreadySavedUser,
StatusPagePrivateUser,
{
miscData: {
token: token,
token: accessToken,
},
},
);

View File

@@ -4,6 +4,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
@@ -29,7 +30,11 @@ const router: ExpressRouter = Express.getRouter();
router.get(
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM ServiceProviderConfig - scimId: ${req.params["statusPageScimId"]!}`,
@@ -44,7 +49,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -53,7 +58,11 @@ router.get(
router.get(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -164,7 +173,7 @@ router.get(
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -173,7 +182,11 @@ router.get(
router.get(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -231,7 +244,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -240,7 +253,11 @@ router.get(
router.post(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -333,7 +350,7 @@ router.post(
return Response.sendJsonObjectResponse(req, res, createdUser);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -342,7 +359,11 @@ router.post(
router.put(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -489,7 +510,7 @@ router.put(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -498,7 +519,11 @@ router.put(
router.delete(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -562,7 +587,7 @@ router.delete(
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);

View File

@@ -1,6 +1,5 @@
import SSOUtil from "../Utils/SSO";
import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
import Email from "Common/Types/Email";
import BadRequestException from "Common/Types/Exception/BadRequestException";
import Exception from "Common/Types/Exception/Exception";
@@ -8,9 +7,11 @@ import ServerException from "Common/Types/Exception/ServerException";
import HashedString from "Common/Types/HashedString";
import { JSONObject } from "Common/Types/JSON";
import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
import StatusPagePrivateUserSessionService, {
SessionMetadata as StatusPageSessionMetadata,
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
import StatusPageService from "Common/Server/Services/StatusPageService";
import StatusPageSsoService from "Common/Server/Services/StatusPageSsoService";
import CookieUtil from "Common/Server/Utils/Cookie";
@@ -19,8 +20,10 @@ import Express, {
ExpressResponse,
ExpressRouter,
NextFunction,
extractDeviceInfo,
getClientIp,
headerValueToString,
} from "Common/Server/Utils/Express";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
@@ -30,6 +33,8 @@ import xml2js from "xml2js";
// Initialize Express router.
const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
// Define a GET route for SSO in a status page context.
router.get(
"/status-page-sso/:statusPageId/:statusPageSsoId",
@@ -115,7 +120,11 @@ router.get(
router.post(
"/status-page-idp-login/:statusPageId/:statusPageSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const samlResponseBase64: string = req.body.SAMLResponse;
@@ -281,24 +290,30 @@ router.post(
});
}
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30),
),
if (!alreadySavedUser.projectId) {
alreadySavedUser.projectId = projectId;
}
const sessionMetadata: StatusPageSessionMetadata =
await StatusPagePrivateUserSessionService.createSession({
projectId: alreadySavedUser.projectId!,
statusPageId: statusPageId,
statusPagePrivateUserId: alreadySavedUser.id!,
ipAddress: getClientIp(req),
userAgent: headerValueToString(req.headers["user-agent"]),
...extractDeviceInfo(req),
});
const token: string = CookieUtil.setStatusPagePrivateUserCookie({
expressResponse: res,
user: alreadySavedUser,
statusPageId: statusPageId,
sessionId: sessionMetadata.session.id!,
refreshToken: sessionMetadata.refreshToken,
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
},
);
// get status page URL.
const statusPageURL: string =
await StatusPageService.getStatusPageFirstURL(statusPageId);
@@ -312,7 +327,11 @@ router.post(
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);

View File

@@ -12,6 +12,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -22,139 +23,146 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/make-call",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await CallService.makeCall(body["callRequest"] as CallRequest, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
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,
});
await CallService.makeCall(body["callRequest"] as CallRequest, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
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);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: "This is a test call from OneUptime.",
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
throw new BadDataException(
"Error making test call. Please check the twilio logs for more details",
);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: "This is a test call from OneUptime.",
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Error making test call. Please check the twilio logs for more details",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
});
},
);
export default router;

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
@@ -19,70 +20,74 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const mail: EmailMessage = {
templateType: body["templateType"] as EmailTemplateType,
toEmail: new Email(body["toEmail"] as string),
subject: body["subject"] as string,
vars: body["vars"] as Dictionary<string>,
body: (body["body"] as string) || "",
};
const mail: EmailMessage = {
templateType: body["templateType"] as EmailTemplateType,
toEmail: new Email(body["toEmail"] as string),
subject: body["subject"] as string,
vars: body["vars"] as Dictionary<string>,
body: (body["body"] as string) || "",
};
let mailServer: EmailServer | undefined = undefined;
let mailServer: EmailServer | undefined = undefined;
if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getEmailServer(req.body);
if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getEmailServer(req.body);
}
await MailService.send(mail, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
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);
} catch (err) {
return next(err);
}
await MailService.send(mail, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
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

@@ -5,6 +5,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { JSONObject } from "Common/Types/JSON";
@@ -15,50 +16,54 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
// Support both new devices format and legacy deviceTokens/deviceNames format
let devices: Array<{ token: string; name?: string }> = [];
// 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.");
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);
} catch (err) {
return next(err);
}
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);
},
);

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -21,130 +22,141 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
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,
});
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
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);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
await SmsService.sendSms(
toPhone,
"This is a test SMS from OneUptime.",
{
projectId: config.projectId,
customTwilioConfig: twilioConfig,
},
);
} catch (err) {
logger.error(err);
throw new BadDataException(
"Failed to send test SMS. Please check the twilio logs for more details.",
);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await SmsService.sendSms(toPhone, "This is a test SMS from OneUptime.", {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Failed to send test SMS. Please check the twilio logs for more details.",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
});
},
);
export default router;

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -18,87 +19,92 @@ import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
const router: ExpressRouter = Express.getRouter();
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const smtpConfigId: ObjectID = new ObjectID(body["smtpConfigId"] as string);
const smtpConfigId: ObjectID = new ObjectID(
body["smtpConfigId"] as string,
);
const config: ProjectSmtpConfig | null =
await ProjectSMTPConfigService.findOneById({
id: smtpConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
hostname: true,
port: true,
username: true,
password: true,
fromEmail: true,
fromName: true,
secure: true,
projectId: true,
},
});
const config: ProjectSmtpConfig | null =
await ProjectSMTPConfigService.findOneById({
id: smtpConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
hostname: true,
port: true,
username: true,
password: true,
fromEmail: true,
fromName: true,
secure: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"smtp-config not found for id" + smtpConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"smtp-config not found for id" + smtpConfigId.toString(),
),
);
}
const toEmail: Email = new Email(body["toEmail"] as string);
const toEmail: Email = new Email(body["toEmail"] as string);
if (!toEmail) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toEmail is required"),
);
}
if (!toEmail) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toEmail is required"),
);
}
const mail: EmailMessage = {
templateType: EmailTemplateType.SMTPTest,
toEmail: new Email(body["toEmail"] as string),
subject: "Test Email from OneUptime",
vars: {},
body: "",
};
const mail: EmailMessage = {
templateType: EmailTemplateType.SMTPTest,
toEmail: new Email(body["toEmail"] as string),
subject: "Test Email from OneUptime",
vars: {},
body: "",
};
const mailServer: EmailServer = {
id: config.id!,
host: config.hostname!,
port: config.port!,
username: config.username!,
password: config.password!,
fromEmail: config.fromEmail!,
fromName: config.fromName!,
secure: Boolean(config.secure),
};
const mailServer: EmailServer = {
id: config.id!,
host: config.hostname!,
port: config.port!,
username: config.username!,
password: config.password!,
fromEmail: config.fromEmail!,
fromName: config.fromName!,
secure: Boolean(config.secure),
};
try {
await MailService.send(mail, {
emailServer: mailServer,
projectId: config.projectId!,
timeout: 4000,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
),
);
}
try {
await MailService.send(mail, {
emailServer: mailServer,
projectId: config.projectId!,
timeout: 4000,
});
} catch (err) {
logger.error(err);
throw new BadDataException(
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
);
}
return Response.sendEmptySuccessResponse(req, res);
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -0,0 +1,491 @@
import WhatsAppService from "../Services/WhatsAppService";
import BadDataException from "Common/Types/Exception/BadDataException";
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import ObjectID from "Common/Types/ObjectID";
import Phone from "Common/Types/Phone";
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
import {
WhatsAppTemplateId,
WhatsAppTemplateIds,
WhatsAppTemplateLanguage,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
import GlobalConfigService from "Common/Server/Services/GlobalConfigService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
const router: ExpressRouter = Express.getRouter();
const MAX_STATUS_MESSAGE_LENGTH: number = 500;
export const mapWebhookStatusToWhatsAppStatus: (
status?: string,
) => WhatsAppStatus = (status?: string): WhatsAppStatus => {
switch ((status || "").toLowerCase()) {
case "sent":
return WhatsAppStatus.Sent;
case "delivered":
return WhatsAppStatus.Delivered;
case "read":
return WhatsAppStatus.Read;
case "failed":
return WhatsAppStatus.Failed;
case "deleted":
case "removed":
return WhatsAppStatus.Deleted;
case "warning":
return WhatsAppStatus.Warning;
case "queued":
case "pending":
return WhatsAppStatus.Queued;
case "error":
return WhatsAppStatus.Error;
case "success":
return WhatsAppStatus.Success;
default:
return WhatsAppStatus.Unknown;
}
};
export const buildStatusMessage: (payload: JSONObject) => string | undefined = (
payload: JSONObject,
): string | undefined => {
const messageParts: Array<string> = [];
const rawStatus: string | undefined = payload["status"]
? String(payload["status"])
: undefined;
if (rawStatus) {
messageParts.push(`Status: ${rawStatus}`);
}
const timestamp: string | undefined = payload["timestamp"]
? String(payload["timestamp"])
: undefined;
if (timestamp) {
const numericTimestamp: number = Number(timestamp);
if (!isNaN(numericTimestamp)) {
messageParts.push(
`Timestamp: ${new Date(numericTimestamp * 1000).toISOString()}`,
);
} else {
messageParts.push(`Timestamp: ${timestamp}`);
}
}
const conversation: JSONObject | undefined =
(payload["conversation"] as JSONObject | undefined) || undefined;
if (conversation) {
if (conversation["id"]) {
messageParts.push(`Conversation: ${conversation["id"]}`);
}
const origin: JSONObject | undefined =
(conversation["origin"] as JSONObject | undefined) || undefined;
if (origin?.["type"]) {
messageParts.push(`Origin: ${origin["type"]}`);
}
if (conversation["expiration_timestamp"]) {
const expirationTimestamp: number = Number(
conversation["expiration_timestamp"],
);
if (!isNaN(expirationTimestamp)) {
messageParts.push(
`Conversation expires: ${new Date(expirationTimestamp * 1000).toISOString()}`,
);
}
}
}
const pricing: JSONObject | undefined =
(payload["pricing"] as JSONObject | undefined) || undefined;
if (pricing) {
const pricingParts: Array<string> = [];
if (pricing["billable"] !== undefined) {
pricingParts.push(`billable=${pricing["billable"]}`);
}
if (pricing["category"]) {
pricingParts.push(`category=${pricing["category"]}`);
}
if (pricing["pricing_model"]) {
pricingParts.push(`model=${pricing["pricing_model"]}`);
}
if (pricingParts.length > 0) {
messageParts.push(`Pricing: ${pricingParts.join(", ")}`);
}
}
const errors: JSONArray | undefined =
(payload["errors"] as JSONArray | undefined) || undefined;
if (Array.isArray(errors) && errors.length > 0) {
const firstError: JSONObject = errors[0] as JSONObject;
const errorParts: Array<string> = [];
if (firstError["title"]) {
errorParts.push(String(firstError["title"]));
}
if (firstError["code"]) {
errorParts.push(`code=${firstError["code"]}`);
}
if (firstError["detail"]) {
errorParts.push(String(firstError["detail"]));
}
if (errorParts.length > 0) {
messageParts.push(`Error: ${errorParts.join(" - ")}`);
}
}
if (messageParts.length === 0) {
return undefined;
}
const statusMessage: string = messageParts.join(" | ");
if (statusMessage.length <= MAX_STATUS_MESSAGE_LENGTH) {
return statusMessage;
}
return `${statusMessage.substring(0, MAX_STATUS_MESSAGE_LENGTH - 3)}...`;
};
const updateWhatsAppLogStatus: (
statusPayload: JSONObject,
) => Promise<void> = async (statusPayload: JSONObject): Promise<void> => {
const messageId: string | undefined = statusPayload["id"]
? String(statusPayload["id"])
: undefined;
if (!messageId) {
logger.warn(
`[Meta WhatsApp Webhook] Received status payload without message id. Payload: ${JSON.stringify(statusPayload)}`,
);
return;
}
const rawStatus: string | undefined = statusPayload["status"]
? String(statusPayload["status"])
: undefined;
const derivedStatus: WhatsAppStatus =
mapWebhookStatusToWhatsAppStatus(rawStatus);
const statusMessage: string | undefined = buildStatusMessage(statusPayload);
const updateResult: number = await WhatsAppLogService.updateOneBy({
query: {
whatsAppMessageId: messageId,
},
data: {
status: derivedStatus,
...(statusMessage ? { statusMessage } : {}),
},
props: {
isRoot: true,
},
});
if (updateResult === 0) {
logger.warn(
`[Meta WhatsApp Webhook] No WhatsApp log found for message id ${messageId}. Payload: ${JSON.stringify(statusPayload)}`,
);
} else {
logger.debug(
`[Meta WhatsApp Webhook] Updated WhatsApp log for message id ${messageId} with status ${derivedStatus}.`,
);
}
};
const toTemplateVariables: (
rawVariables: JSONObject | undefined,
) => Record<string, string> | undefined = (
rawVariables: JSONObject | undefined,
): Record<string, string> | undefined => {
if (!rawVariables) {
return undefined;
}
const result: Record<string, string> = {};
for (const key of Object.keys(rawVariables)) {
const value: unknown = rawVariables[key];
if (value !== null && value !== undefined) {
result[key] = String(value);
}
}
return Object.keys(result).length > 0 ? result : undefined;
};
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
const body: JSONObject = req.body as JSONObject;
if (!body["to"]) {
throw new BadDataException("`to` phone number is required");
}
const toPhone: Phone = new Phone(body["to"] as string);
const message: WhatsAppMessage = {
to: toPhone,
body: (body["body"] as string) || "",
templateKey: (body["templateKey"] as string) || undefined,
templateVariables: toTemplateVariables(
body["templateVariables"] as JSONObject | undefined,
),
templateLanguageCode:
(body["templateLanguageCode"] as string) || undefined,
};
try {
await WhatsAppService.sendWhatsApp(message, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId: body["userOnCallLogTimelineId"]
? new ObjectID(body["userOnCallLogTimelineId"] as string)
: undefined,
incidentId: body["incidentId"]
? new ObjectID(body["incidentId"] as string)
: undefined,
alertId: body["alertId"]
? new ObjectID(body["alertId"] as string)
: undefined,
scheduledMaintenanceId: body["scheduledMaintenanceId"]
? new ObjectID(body["scheduledMaintenanceId"] as string)
: undefined,
statusPageId: body["statusPageId"]
? new ObjectID(body["statusPageId"] as string)
: undefined,
statusPageAnnouncementId: body["statusPageAnnouncementId"]
? new ObjectID(body["statusPageAnnouncementId"] as string)
: undefined,
userId: body["userId"]
? new ObjectID(body["userId"] as string)
: undefined,
onCallPolicyId: body["onCallPolicyId"]
? new ObjectID(body["onCallPolicyId"] as string)
: undefined,
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
? new ObjectID(body["onCallPolicyEscalationRuleId"] as string)
: undefined,
onCallDutyPolicyExecutionLogTimelineId: body[
"onCallDutyPolicyExecutionLogTimelineId"
]
? new ObjectID(
body["onCallDutyPolicyExecutionLogTimelineId"] as string,
)
: undefined,
onCallScheduleId: body["onCallScheduleId"]
? new ObjectID(body["onCallScheduleId"] as string)
: undefined,
teamId: body["teamId"]
? new ObjectID(body["teamId"] as string)
: undefined,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.get("/webhook", async (req: ExpressRequest, res: ExpressResponse) => {
const mode: string | undefined = req.query["hub.mode"]
? String(req.query["hub.mode"])
: undefined;
const verifyToken: string | undefined = req.query["hub.verify_token"]
? String(req.query["hub.verify_token"])
: undefined;
const challenge: string | undefined = req.query["hub.challenge"]
? String(req.query["hub.challenge"])
: undefined;
if (mode === "subscribe" && challenge) {
const globalConfigTokenResponse: GlobalConfig | null =
await GlobalConfigService.findOneBy({
query: {
_id: ObjectID.getZeroObjectID().toString(),
},
props: {
isRoot: true,
},
select: {
metaWhatsAppWebhookVerifyToken: true,
},
});
const configuredVerifyToken: string | undefined =
globalConfigTokenResponse?.metaWhatsAppWebhookVerifyToken?.trim() ||
undefined;
if (!configuredVerifyToken) {
logger.error(
"Meta WhatsApp webhook verify token is not configured. Rejecting verification request.",
);
res.sendStatus(403);
return;
}
if (verifyToken === configuredVerifyToken) {
res.status(200).send(challenge);
return;
}
logger.warn(
"Meta WhatsApp webhook verification failed due to token mismatch.",
);
res.sendStatus(403);
return;
}
res.sendStatus(400);
});
router.post(
"/webhook",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body as JSONObject;
if (
(body["object"] as string | undefined) !== "whatsapp_business_account"
) {
logger.debug(
`[Meta WhatsApp Webhook] Received event for unsupported object: ${JSON.stringify(body)}`,
);
return Response.sendEmptySuccessResponse(req, res);
}
const entries: JSONArray | undefined = body["entry"] as
| JSONArray
| undefined;
if (!Array.isArray(entries)) {
logger.warn(
`[Meta WhatsApp Webhook] Payload did not include entries array. Payload: ${JSON.stringify(body)}`,
);
return Response.sendEmptySuccessResponse(req, res);
}
const statusUpdatePromises: Array<Promise<void>> = [];
for (const entry of entries) {
const entryObject: JSONObject = entry as JSONObject;
const changes: JSONArray | undefined =
(entryObject["changes"] as JSONArray | undefined) || undefined;
if (!Array.isArray(changes)) {
continue;
}
for (const change of changes) {
const changeObject: JSONObject = change as JSONObject;
const value: JSONObject | undefined =
(changeObject["value"] as JSONObject | undefined) || undefined;
if (!value) {
continue;
}
const statuses: JSONArray | undefined =
(value["statuses"] as JSONArray | undefined) || undefined;
if (Array.isArray(statuses)) {
for (const statusItem of statuses) {
statusUpdatePromises.push(
updateWhatsAppLogStatus(statusItem as JSONObject),
);
}
}
}
}
if (statusUpdatePromises.length > 0) {
await Promise.allSettled(statusUpdatePromises);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body as JSONObject;
if (!body["toPhone"]) {
throw new BadDataException("toPhone is required");
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const templateKey: WhatsAppTemplateId =
WhatsAppTemplateIds.TestNotification;
const templateLanguageCode: string =
WhatsAppTemplateLanguage[templateKey] || "en";
const message: WhatsAppMessage = {
to: toPhone,
body: "",
templateKey,
templateVariables: undefined,
templateLanguageCode,
};
try {
await WhatsAppService.sendWhatsApp(message, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
isSensitive: false,
});
} catch (err) {
const errorMsg: string =
err instanceof Error && err.message
? err.message
: "Failed to send test WhatsApp message.";
throw new BadDataException(errorMsg);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -12,6 +12,8 @@ import Phone from "Common/Types/Phone";
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
export const DEFAULT_META_WHATSAPP_API_VERSION: string = "v23.0";
export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
async (): Promise<EmailServer | null> => {
const globalConfig: GlobalConfig | null =
@@ -222,6 +224,83 @@ export const SMSHighRiskCostInCents: number = process.env[
? parseInt(process.env["SMS_HIGH_RISK_COST_IN_CENTS"])
: 0;
export interface MetaWhatsAppConfig {
accessToken: string;
phoneNumberId: string;
businessAccountId?: string | undefined;
appId?: string | undefined;
appSecret?: string | undefined;
apiVersion?: string | undefined;
}
type GetMetaWhatsAppConfigFunction = () => Promise<MetaWhatsAppConfig>;
export const getMetaWhatsAppConfig: GetMetaWhatsAppConfigFunction =
async (): Promise<MetaWhatsAppConfig> => {
const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({
query: {
_id: ObjectID.getZeroObjectID().toString(),
},
props: {
isRoot: true,
},
select: {
metaWhatsAppAccessToken: true,
metaWhatsAppPhoneNumberId: true,
metaWhatsAppBusinessAccountId: true,
metaWhatsAppAppId: true,
metaWhatsAppAppSecret: true,
},
});
if (!globalConfig) {
throw new BadDataException("Global Config not found");
}
const accessToken: string | undefined =
globalConfig.metaWhatsAppAccessToken?.trim();
const phoneNumberId: string | undefined =
globalConfig.metaWhatsAppPhoneNumberId?.trim();
if (!accessToken) {
throw new BadDataException(
"Meta WhatsApp access token not configured. Please set it in the Admin Dashboard: " +
AdminDashboardClientURL.toString(),
);
}
if (!phoneNumberId) {
throw new BadDataException(
"Meta WhatsApp phone number ID not configured. Please set it in the Admin Dashboard: " +
AdminDashboardClientURL.toString(),
);
}
const businessAccountId: string | undefined =
globalConfig.metaWhatsAppBusinessAccountId?.trim() || undefined;
const appId: string | undefined =
globalConfig.metaWhatsAppAppId?.trim() || undefined;
const appSecret: string | undefined =
globalConfig.metaWhatsAppAppSecret?.trim() || undefined;
const apiVersion: string = DEFAULT_META_WHATSAPP_API_VERSION;
return {
accessToken,
phoneNumberId,
businessAccountId,
appId,
appSecret,
apiVersion,
};
};
export const WhatsAppTextDefaultCostInCents: number = process.env[
"WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"
]
? parseInt(process.env["WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"])
: 0;
export const CallHighRiskCostInCentsPerMinute: number = process.env[
"CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"
]

View File

@@ -2,6 +2,7 @@ import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
import SmsAPI from "./API/SMS";
import WhatsAppAPI from "./API/WhatsApp";
import PushNotificationAPI from "./API/PushNotification";
import SMTPConfigAPI from "./API/SMTPConfig";
import "./Utils/Handlebars";
@@ -16,6 +17,7 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
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

@@ -37,11 +37,49 @@ class TransporterPool {
private static semaphore: Map<string, number> = new Map();
private static readonly MAX_CONCURRENT_CONNECTIONS = 100;
private static resolveConnectionSettings(emailServer: EmailServer): {
portNumber: number;
wantsSecureConnection: boolean;
secureConnection: boolean;
requireTLS: boolean;
mode: "implicit-tls" | "starttls" | "plain";
} {
const portNumber: number = emailServer.port.toNumber();
const wantsSecureConnection: boolean = emailServer.secure;
const isImplicitTLSPort: boolean = portNumber === 465;
const secureConnection: boolean = isImplicitTLSPort;
const requireTLS: boolean = wantsSecureConnection && !isImplicitTLSPort;
let mode: "implicit-tls" | "starttls" | "plain" = "plain";
if (secureConnection) {
mode = "implicit-tls";
} else if (requireTLS) {
mode = "starttls";
}
return {
portNumber,
wantsSecureConnection,
secureConnection,
requireTLS,
mode,
};
}
private static getPoolKey(emailServer: EmailServer): string {
const { portNumber, mode } = this.resolveConnectionSettings(emailServer);
const username: string = emailServer.username || "noauth";
return `${emailServer.host.toString()}:${portNumber}:${username}:${mode}`;
}
public static getTransporter(
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
if (!this.pools.has(key)) {
const transporter: Transporter = this.createTransporter(
@@ -59,9 +97,12 @@ class TransporterPool {
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
const { portNumber, wantsSecureConnection, secureConnection, requireTLS } =
this.resolveConnectionSettings(emailServer);
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
if (!emailServer.secure) {
if (!wantsSecureConnection) {
tlsOptions = {
rejectUnauthorized: false,
};
@@ -69,8 +110,9 @@ class TransporterPool {
return nodemailer.createTransport({
host: emailServer.host.toString(),
port: emailServer.port.toNumber(),
secure: emailServer.secure,
port: portNumber,
secure: secureConnection,
requireTLS,
tls: tlsOptions,
auth:
emailServer.username && emailServer.password
@@ -88,7 +130,7 @@ class TransporterPool {
public static async acquireConnection(
emailServer: EmailServer,
): Promise<void> {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
while ((this.semaphore.get(key) || 0) >= this.MAX_CONCURRENT_CONNECTIONS) {
await new Promise<void>((resolve: () => void) => {
@@ -100,7 +142,7 @@ class TransporterPool {
}
public static releaseConnection(emailServer: EmailServer): void {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
const current: number = this.semaphore.get(key) || 0;
this.semaphore.set(key, Math.max(0, current - 1));
}

View File

@@ -0,0 +1,497 @@
import {
WhatsAppTextDefaultCostInCents,
getMetaWhatsAppConfig,
MetaWhatsAppConfig,
DEFAULT_META_WHATSAPP_API_VERSION,
} from "../Config";
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
import {
AuthenticationTemplates,
WhatsAppTemplateId,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import NotificationService from "Common/Server/Services/NotificationService";
import ProjectService from "Common/Server/Services/ProjectService";
import UserOnCallLogTimelineService from "Common/Server/Services/UserOnCallLogTimelineService";
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
import logger from "Common/Server/Utils/Logger";
import Project from "Common/Models/DatabaseModels/Project";
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
import API from "Common/Utils/API";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import Protocol from "Common/Types/API/Protocol";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
const SENSITIVE_MESSAGE_PLACEHOLDER: string =
"This message is sensitive and is not logged";
export default class WhatsAppService {
public static async sendWhatsApp(
message: WhatsAppMessage,
options: {
projectId?: ObjectID | undefined;
isSensitive?: boolean | undefined;
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> {
let sendError: Error | null = null;
const whatsAppLog: WhatsAppLog = new WhatsAppLog();
try {
if (!message.to) {
throw new BadDataException(
"WhatsApp recipient phone number is required",
);
}
if (!message.body && !message.templateKey) {
throw new BadDataException(
"Either WhatsApp message body or template key must be provided",
);
}
const isSensitiveMessage: boolean = Boolean(options.isSensitive);
const messageSummary: string = isSensitiveMessage
? SENSITIVE_MESSAGE_PLACEHOLDER
: message.body ||
(message.templateKey
? `Template: ${message.templateKey}${
message.templateVariables
? " Variables: " + JSON.stringify(message.templateVariables)
: ""
}`
: "");
whatsAppLog.toNumber = message.to;
whatsAppLog.messageText = messageSummary;
whatsAppLog.whatsAppCostInUSDCents = 0;
if (options.projectId) {
whatsAppLog.projectId = options.projectId;
}
if (options.incidentId) {
whatsAppLog.incidentId = options.incidentId;
}
if (options.alertId) {
whatsAppLog.alertId = options.alertId;
}
if (options.scheduledMaintenanceId) {
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}
if (options.statusPageId) {
whatsAppLog.statusPageId = options.statusPageId;
}
if (options.statusPageAnnouncementId) {
whatsAppLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
}
if (options.userId) {
whatsAppLog.userId = options.userId;
}
if (options.teamId) {
whatsAppLog.teamId = options.teamId;
}
if (options.onCallPolicyId) {
whatsAppLog.onCallDutyPolicyId = options.onCallPolicyId;
}
if (options.onCallPolicyEscalationRuleId) {
whatsAppLog.onCallDutyPolicyEscalationRuleId =
options.onCallPolicyEscalationRuleId;
}
if (options.onCallScheduleId) {
whatsAppLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
}
const config: MetaWhatsAppConfig = await getMetaWhatsAppConfig();
let messageCost: number = 0;
const shouldChargeForMessage: boolean = IsBillingEnabled;
if (shouldChargeForMessage) {
messageCost = WhatsAppTextDefaultCostInCents / 100;
}
let project: Project | null = null;
if (options.projectId) {
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
if (!project) {
whatsAppLog.status = WhatsAppStatus.Error;
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
return;
}
if (shouldChargeForMessage) {
let updatedBalance: number =
project.smsOrCallCurrentBalanceInUSDCents || 0;
try {
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
project.id!,
);
} catch (err) {
logger.error(err);
}
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
if (!project.smsOrCallCurrentBalanceInUSDCents) {
whatsAppLog.status = WhatsAppStatus.LowBalance;
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} does not have enough balance for WhatsApp messages.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
id: project.id!,
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
`Low WhatsApp message balance for ${project.name || ""}`,
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
} USD. Required balance for this message is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
if (project.smsOrCallCurrentBalanceInUSDCents < messageCost * 100) {
whatsAppLog.status = WhatsAppStatus.LowBalance;
whatsAppLog.statusMessage = `Project does not have enough balance to send WhatsApp message. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${messageCost} USD.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
id: project.id!,
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
`Low WhatsApp message balance for ${project.name || ""}`,
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
}
}
const payload: JSONObject = {
messaging_product: "whatsapp",
recipient_type: "individual",
to: message.to.toString(),
} as JSONObject;
if (!message.templateKey) {
throw new BadDataException("WhatsApp message template key is required");
}
if (message.templateKey) {
const template: JSONObject = {
name: message.templateKey,
language: {
code: message.templateLanguageCode || "en",
},
} as JSONObject;
const components: JSONArray = [];
if (
message.templateVariables &&
Object.keys(message.templateVariables).length > 0
) {
const parameters: JSONArray = [];
for (const [key, value] of Object.entries(
message.templateVariables,
)) {
parameters.push({
type: "text",
parameter_name: key,
text: value,
} as JSONObject);
}
if (parameters.length > 0) {
components.push({
type: "body",
parameters,
} as JSONObject);
}
}
/*
* Check if this is an authentication template
* Authentication templates may have special requirements including button components
*/
const isAuthTemplate: boolean = AuthenticationTemplates.has(
message.templateKey as WhatsAppTemplateId,
);
if (isAuthTemplate) {
logger.info(
`Sending authentication template: ${message.templateKey}`,
);
/*
* Authentication templates in WhatsApp may have a button component for "Copy Code"
* If the template was created with a button, we need to provide button parameters
*/
if (message.templateVariables) {
const otpCode: string | undefined =
message.templateVariables["1"] ||
message.templateVariables["otp"] ||
message.templateVariables["code"];
if (otpCode) {
/*
* Add button component - the index should match the button position in the template
* Usually authentication templates have the button as the first (and only) button
*/
components.push({
type: "button",
sub_type: "url",
index: 0,
parameters: [
{
type: "text",
text: otpCode,
},
],
} as JSONObject);
}
}
}
if (components.length > 0) {
template["components"] = components;
}
payload["type"] = "template";
payload["template"] = template;
} else {
payload["type"] = "text";
payload["text"] = {
body: message.body || "",
} as JSONObject;
}
const apiVersion: string =
config.apiVersion?.trim() || DEFAULT_META_WHATSAPP_API_VERSION;
const url: URL = new URL(
Protocol.HTTPS,
"graph.facebook.com",
new Route(`${apiVersion}/${config.phoneNumberId}/messages`),
);
logger.debug(`WhatsApp API request: ${JSON.stringify(payload)}`);
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post<JSONObject>({
url,
data: payload,
headers: {
Authorization: `Bearer ${config.accessToken}`,
"Content-Type": "application/json",
},
});
if (response instanceof HTTPErrorResponse) {
logger.error("Failed to send WhatsApp message.");
logger.error(response);
const responseDataAsJSONObject: JSONObject = response.data;
const responseJsonAsJSONObject: JSONObject | undefined =
(response.jsonData as JSONObject | undefined) || undefined;
// Log full error details for debugging
const errorObject: JSONObject | undefined =
(responseDataAsJSONObject["error"] as JSONObject | undefined) ||
(responseJsonAsJSONObject?.["error"] as JSONObject | undefined);
if (errorObject) {
logger.error("WhatsApp API Error Details:");
logger.error(JSON.stringify(errorObject, null, 2));
}
const detailedErrorMessage: string | undefined =
((responseDataAsJSONObject["error"] as JSONObject | undefined)?.[
"message"
] as string | undefined) ||
((responseJsonAsJSONObject?.["error"] as JSONObject | undefined)?.[
"message"
] as string | undefined);
throw new BadDataException(
detailedErrorMessage || "Failed to send WhatsApp message.",
);
}
const responseData: JSONObject = (response.jsonData || {}) as JSONObject;
let messageId: string | undefined = undefined;
const messagesArray: JSONArray | undefined =
(responseData["messages"] as JSONArray) || undefined;
if (Array.isArray(messagesArray) && messagesArray.length > 0) {
const firstMessage: JSONObject = messagesArray[0] as JSONObject;
if (firstMessage["id"]) {
messageId = firstMessage["id"] as string;
}
}
if (messageId) {
whatsAppLog.whatsAppMessageId = messageId;
}
whatsAppLog.status = WhatsAppStatus.Sent;
whatsAppLog.statusMessage = messageId
? `Message ID: ${messageId}`
: "WhatsApp message sent successfully";
if (shouldChargeForMessage && project) {
const deduction: number = Math.floor(messageCost * 100);
whatsAppLog.whatsAppCostInUSDCents = deduction;
project.smsOrCallCurrentBalanceInUSDCents = Math.max(
0,
Math.floor(
(project.smsOrCallCurrentBalanceInUSDCents || 0) - deduction,
),
);
await ProjectService.updateOneById({
id: project.id!,
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false,
},
props: {
isRoot: true,
},
});
}
} catch (error: any) {
logger.error("Failed to send WhatsApp message.");
logger.error(error);
whatsAppLog.whatsAppCostInUSDCents = 0;
whatsAppLog.status = WhatsAppStatus.Error;
const errorMessage: string =
error && error.message ? error.message.toString() : `${error}`;
whatsAppLog.statusMessage = errorMessage;
sendError = error;
}
if (options.projectId) {
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
}
if (options.userOnCallLogTimelineId) {
await UserOnCallLogTimelineService.updateOneById({
id: options.userOnCallLogTimelineId,
data: {
status: [
WhatsAppStatus.Success,
WhatsAppStatus.Sent,
WhatsAppStatus.Delivered,
WhatsAppStatus.Read,
].includes(whatsAppLog.status || WhatsAppStatus.Error)
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: whatsAppLog.statusMessage,
},
props: {
isRoot: true,
},
});
}
if (sendError) {
throw sendError;
}
}
}

530
App/package-lock.json generated
View File

@@ -41,7 +41,7 @@
"@elastic/elasticsearch": "^8.12.1",
"@monaco-editor/react": "^4.4.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.52.1",
"@opentelemetry/api-logs": "^0.206.0",
"@opentelemetry/context-zone": "^1.25.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
@@ -59,7 +59,9 @@
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
"@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2",
"@types/qrcode": "^1.5.5",
"@types/react-highlight": "^0.12.8",
@@ -68,8 +70,10 @@
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"bullmq": "^5.3.3",
"archiver": "^7.0.1",
"axios": "^1.12.0",
"botbuilder": "^4.23.3",
"bullmq": "^5.61.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
@@ -90,11 +94,11 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.10",
"nodemailer": "^7.0.7",
"otpauth": "^9.3.1",
"pg": "^8.7.3",
"playwright": "^1.50.0",
"posthog-js": "^1.139.6",
"playwright": "^1.56.0",
"posthog-js": "^1.275.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -109,7 +113,7 @@
"react-router-dom": "^6.24.1",
"react-select": "^5.4.0",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"react-syntax-highlighter": "^16.0.0",
"react-toggle": "^4.1.3",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
@@ -124,13 +128,13 @@
"tailwind-merge": "^2.5.2",
"tippy.js": "^6.3.7",
"twilio": "^4.22.0",
"typeorm": "^0.3.20",
"typeorm": "^0.3.26",
"typeorm-extension": "^2.2.13",
"universal-cookie": "^7.2.1",
"use-async-effect": "^2.2.6",
"uuid": "^8.3.2",
"web-push": "^3.6.7",
"zod": "^3.25.30"
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
@@ -253,89 +257,20 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
@@ -511,19 +446,21 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -538,109 +475,28 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz",
"integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.6",
"@babel/types": "^7.23.6"
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -811,14 +667,15 @@
}
},
"node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -846,14 +703,14 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1745,15 +1602,17 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1863,21 +1722,23 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -1948,6 +1809,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2108,6 +1982,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -2136,10 +2011,11 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -2201,6 +2077,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -2231,6 +2108,20 @@
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -2240,9 +2131,10 @@
}
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
@@ -2286,6 +2178,51 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -2389,9 +2326,10 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2408,10 +2346,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -2433,15 +2372,16 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -2452,12 +2392,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -2511,14 +2454,24 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2533,6 +2486,19 @@
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -2587,11 +2553,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2642,10 +2609,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2653,10 +2621,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2665,9 +2637,10 @@
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2826,6 +2799,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -3523,7 +3497,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.1",
@@ -3780,6 +3755,15 @@
"tmpl": "1.0.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -3787,12 +3771,13 @@
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -3803,6 +3788,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -3811,6 +3797,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@@ -3875,9 +3862,10 @@
"dev": true
},
"node_modules/nodemailer": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
@@ -4120,10 +4108,11 @@
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -4627,20 +4616,12 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -4910,9 +4891,10 @@
}
},
"node_modules/xml-crypto": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz",
"integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.1.tgz",
"integrity": "sha512-0GUNbPtQt+PLMsC5HoZRONX+K6NBJEqpXe/lsvrFj0EqfpGPpVfJKGE7a5jCg8s2+Wkrf/2U1G41kIH+zC9eyQ==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.8.8",
"xpath": "0.0.32"

View File

@@ -18,8 +18,6 @@
"dependencies": {
"@sendgrid/mail": "^8.1.0",
"Common": "file:../Common",
"ejs": "^3.1.9",
"handlebars": "^4.7.8",
"nodemailer": "^6.9.7",

View File

@@ -3,6 +3,8 @@ import Route from "../../../Types/API/Route";
import AnalyticsTableEngine from "../../../Types/AnalyticsDatabase/AnalyticsTableEngine";
import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
import Projection from "../../../Types/AnalyticsDatabase/Projection";
import MaterializedView from "../../../Types/AnalyticsDatabase/MaterializedView";
import {
ColumnAccessControl,
TableAccessControl,
@@ -40,6 +42,8 @@ export default class AnalyticsBaseModel extends CommonModel {
enableWorkflowOn?: EnableWorkflowOn | undefined;
enableRealtimeEventsOn?: EnableRealtimeEventsOn | undefined;
partitionKey: string;
projections?: Array<Projection> | undefined;
materializedViews?: Array<MaterializedView> | undefined;
}) {
super({
tableColumns: data.tableColumns,
@@ -140,6 +144,8 @@ export default class AnalyticsBaseModel extends CommonModel {
this.crudApiPath = data.crudApiPath;
this.enableRealtimeEventsOn = data.enableRealtimeEventsOn;
this.partitionKey = data.partitionKey;
this.projections = data.projections || [];
this.materializedViews = data.materializedViews || [];
}
private _enableWorkflowOn: EnableWorkflowOn | undefined;
@@ -250,6 +256,22 @@ export default class AnalyticsBaseModel extends CommonModel {
this._crudApiPath = v;
}
private _projections: Array<Projection> = [];
public get projections(): Array<Projection> {
return this._projections;
}
public set projections(v: Array<Projection>) {
this._projections = v;
}
private _materializedViews: Array<MaterializedView> = [];
public get materializedViews(): Array<MaterializedView> {
return this._materializedViews;
}
public set materializedViews(v: Array<MaterializedView>) {
this._materializedViews = v;
}
public getTenantColumn(): AnalyticsTableColumn | null {
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
(column: AnalyticsTableColumn) => {

View File

@@ -9,6 +9,332 @@ import { SpanStatus } from "./Span";
export default class ExceptionInstance extends AnalyticsBaseModel {
public constructor() {
const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const serviceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const timeUnixNanoColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When was the log created?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const exceptionTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "exceptionType",
title: "Exception Type",
description: "Exception Type", // Examples: java.net.ConnectException; OSError; etc.
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const stackTraceColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "stackTrace",
title: "Stack Trace",
description: "Exception Stack Trace", // Examples: Division by zero; Can't convert 'int' object to str implicitly
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const messageColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "message",
title: "Exception Message",
description: "Exception Message", // Examples: Division by zero; Can't convert 'int' object to str implicitly
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const spanStatusCodeColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
{
key: "spanStatusCode",
title: "Span Status Code",
description: "Span Status Code",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
},
);
const escapedColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "escaped",
title: "Exception Escaped",
description: "Exception Escaped", // SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span.
required: false,
type: TableColumnType.Boolean,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const traceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const spanIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const fingerprintColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "fingerprint",
title: "Fingerprint",
description: "Fingerprint of the exception",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
const spanNameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "spanName",
title: "Span Name",
description: "Name of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
});
super({
tableName: "ExceptionItem",
tableEngine: AnalyticsTableEngine.MergeTree,
@@ -45,330 +371,22 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
},
crudApiPath: new Route("/exceptions"),
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When was the log created?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "exceptionType",
title: "Exception Type",
description: "Exception Type", // Examples: java.net.ConnectException; OSError; etc.
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "stackTrace",
title: "Stack Trace",
description: "Exception Stack Trace", // Examples: Division by zero; Can't convert 'int' object to str implicitly
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "message",
title: "Exception Message",
description: "Exception Message", // Examples: Division by zero; Can't convert 'int' object to str implicitly
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "spanStatusCode",
title: "Span Status Code",
description: "Span Status Code",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "escaped",
title: "Exception Escaped",
description: "Exception Escaped", // SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span.
required: false,
type: TableColumnType.Boolean,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "fingerprint",
title: "Fingerprint",
description: "Fingerprint of the exception",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "spanName",
title: "Span Name",
description: "Name of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryException,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryException,
],
update: [],
},
}),
projectIdColumn,
serviceIdColumn,
timeColumn,
timeUnixNanoColumn,
exceptionTypeColumn,
stackTraceColumn,
messageColumn,
spanStatusCodeColumn,
escapedColumn,
traceIdColumn,
spanIdColumn,
fingerprintColumn,
spanNameColumn,
attributesColumn,
],
projections: [],
sortKeys: ["projectId", "time", "serviceId", "fingerprint"],
primaryKeys: ["projectId", "time", "serviceId", "fingerprint"],
partitionKey: "sipHash64(projectId) % 16",

View File

@@ -2,7 +2,6 @@ import AnalyticsBaseModel from "./AnalyticsBaseModel/AnalyticsBaseModel";
import Log from "./Log";
import Metric from "./Metric";
import Span from "./Span";
import TelemetryAttribute from "./TelemetryAttribute";
import ExceptionInstance from "./ExceptionInstance";
import MonitorLog from "./MonitorLog";
@@ -10,7 +9,6 @@ const AnalyticsModels: Array<{ new (): AnalyticsBaseModel }> = [
Log,
Span,
Metric,
TelemetryAttribute,
ExceptionInstance,
MonitorLog,
];

View File

@@ -10,6 +10,264 @@ import LogSeverity from "../../Types/Log/LogSeverity";
export default class Log extends AnalyticsBaseModel {
public constructor() {
const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const serviceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const timeUnixNanoColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When was the log created?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const severityTextColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "severityText",
title: "Severity Text",
description: "Log Severity Text",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const severityNumberColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
{
key: "severityNumber",
title: "Severity Number",
description: "Log Severity Number",
required: true,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
},
);
const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributeKeys",
title: "Attribute Keys",
description: "Attribute keys extracted from attributes",
required: true,
defaultValue: [],
type: TableColumnType.ArrayText,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const traceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const spanIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const bodyColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "body",
title: "Log Body",
description: "Body of the Log",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
super({
tableName: "LogItem",
tableEngine: AnalyticsTableEngine.MergeTree,
@@ -43,238 +301,19 @@ export default class Log extends AnalyticsBaseModel {
pluralName: "Logs",
crudApiPath: new Route("/logs"),
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When was the log created?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "severityText",
title: "Severity Text",
description: "Log Severity Text",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "severityNumber",
title: "Severity Number",
description: "Log Severity Number",
required: true,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "body",
title: "Log Body",
description: "Body of the Log",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
projectIdColumn,
serviceIdColumn,
timeColumn,
timeUnixNanoColumn,
severityTextColumn,
severityNumberColumn,
attributesColumn,
attributeKeysColumn,
traceIdColumn,
spanIdColumn,
bodyColumn,
],
projections: [],
sortKeys: ["projectId", "time", "serviceId"],
primaryKeys: ["projectId", "time", "serviceId"],
partitionKey: "sipHash64(projectId) % 16",
@@ -345,6 +384,14 @@ export default class Log extends AnalyticsBaseModel {
this.setColumnValue("attributes", v);
}
public get attributeKeys(): Array<string> | undefined {
return this.getColumnValue("attributeKeys") as Array<string> | undefined;
}
public set attributeKeys(v: Array<string> | undefined) {
this.setColumnValue("attributeKeys", v);
}
public get traceId(): string | undefined {
return this.getColumnValue("traceId") as string | undefined;
}

View File

@@ -28,6 +28,481 @@ export enum ServiceType {
export default class Metric extends AnalyticsBaseModel {
public constructor() {
const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
// this can also be the monitor id or the telemetry service id.
const serviceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the Metric",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
// this can also be the monitor id or the telemetry service id.
const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "serviceType",
title: "Service Type",
description: "Type of the service that this telemetry belongs to",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
// add name and description
const nameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "name",
title: "Name",
description: "Name of the Metric",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const aggregationTemporalityColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "aggregationTemporality",
title: "Aggregation Temporality",
description: "Aggregation Temporality of this Metric",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const metricPointTypeColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "metricPointType",
title: "Metric Point Type",
description: "Metric Point Type of this Metric",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
// this is end time.
const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When did the Metric happen?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const startTimeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "startTime",
title: "Start Time",
description: "When did the Metric happen?",
required: false,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
// end time.
const timeUnixNanoColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When did the Metric happen?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const startTimeUnixNanoColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "startTimeUnixNano",
title: "Start Time (in Unix Nano)",
description: "When did the Metric happen?",
required: false,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
type: TableColumnType.JSON,
defaultValue: {},
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributeKeys",
title: "Attribute Keys",
description: "Attribute keys extracted from attributes",
required: true,
defaultValue: [],
type: TableColumnType.ArrayText,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const isMonotonicColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "isMonotonic",
title: "Is Monotonic",
description: "Is Monotonic",
required: false,
type: TableColumnType.Boolean,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const countColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "count",
title: "Count",
description: "Count",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const sumColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "sum",
title: "Sum",
description: "Sum",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const valueColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "value",
title: "Value",
description: "Value",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const minColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "min",
title: "Min",
description: "Min",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const maxColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "max",
title: "Max",
description: "Max",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const bucketCountsColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "bucketCounts",
title: "Bucket Counts",
description: "Bucket Counts",
required: true,
defaultValue: [],
type: TableColumnType.ArrayNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
});
const explicitBoundsColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
{
key: "explicitBounds",
title: "Explicit Bonds",
description: "Explicit Bonds",
required: true,
defaultValue: [],
type: TableColumnType.ArrayNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
},
);
super({
tableName: "MetricItem",
tableEngine: AnalyticsTableEngine.MergeTree,
@@ -61,453 +536,28 @@ export default class Metric extends AnalyticsBaseModel {
],
},
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
// this can also be the monitor id or the telemetry service id.
new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the Metric",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
// this can also be the monitor id or the telemetry service id.
new AnalyticsTableColumn({
key: "serviceType",
title: "Service Type",
description: "Type of the service that this telemetry belongs to",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
// add name and description
new AnalyticsTableColumn({
key: "name",
title: "Name",
description: "Name of the Metric",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "aggregationTemporality",
title: "Aggregation Temporality",
description: "Aggregation Temporality of this Metric",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "metricPointType",
title: "Metric Point Type",
description: "Metric Point Type of this Metric",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
// this is end time.
new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When did the Metric happen?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "startTime",
title: "Start Time",
description: "When did the Metric happen?",
required: false,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
// end time.
new AnalyticsTableColumn({
key: "timeUnixNano",
title: "Time (in Unix Nano)",
description: "When did the Metric happen?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "startTimeUnixNano",
title: "Start Time (in Unix Nano)",
description: "When did the Metric happen?",
required: false,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
type: TableColumnType.JSON,
defaultValue: {},
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "isMonotonic",
title: "Is Monotonic",
description: "Is Monotonic",
required: false,
type: TableColumnType.Boolean,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "count",
title: "Count",
description: "Count",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "sum",
title: "Sum",
description: "Sum",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "value",
title: "Value",
description: "Value",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "min",
title: "Min",
description: "Min",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "max",
title: "Max",
description: "Max",
required: false,
type: TableColumnType.Decimal,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "bucketCounts",
title: "Bucket Counts",
description: "Bucket Counts",
required: true,
defaultValue: [],
type: TableColumnType.ArrayNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "explicitBounds",
title: "Explicit Bonds",
description: "Explicit Bonds",
required: true,
defaultValue: [],
type: TableColumnType.ArrayNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceLog,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceLog,
],
update: [],
},
}),
projectIdColumn,
serviceIdColumn,
serviceTypeColumn,
nameColumn,
aggregationTemporalityColumn,
metricPointTypeColumn,
timeColumn,
startTimeColumn,
timeUnixNanoColumn,
startTimeUnixNanoColumn,
attributesColumn,
attributeKeysColumn,
isMonotonicColumn,
countColumn,
sumColumn,
valueColumn,
minColumn,
maxColumn,
bucketCountsColumn,
explicitBoundsColumn,
],
projections: [],
sortKeys: ["projectId", "time", "serviceId"],
primaryKeys: ["projectId", "time", "serviceId"],
partitionKey: "sipHash64(projectId) % 16",
@@ -590,6 +640,14 @@ export default class Metric extends AnalyticsBaseModel {
this.setColumnValue("attributes", v);
}
public get attributeKeys(): Array<string> | undefined {
return this.getColumnValue("attributeKeys") as Array<string> | undefined;
}
public set attributeKeys(v: Array<string> | undefined) {
this.setColumnValue("attributeKeys", v);
}
public get startTime(): Date | undefined {
return this.getColumnValue("startTime") as Date | undefined;
}

View File

@@ -9,6 +9,100 @@ import Permission from "../../Types/Permission";
export default class MonitorLog extends AnalyticsBaseModel {
public constructor() {
const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
});
const monitorIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "monitorId",
title: "Monitor ID",
description: "ID of the monitor which this logs belongs to",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
});
const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
});
const logBodyColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "logBody",
title: "Log Body",
description: "The body of the log",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
});
super({
tableName: "MonitorLog",
tableEngine: AnalyticsTableEngine.MergeTree,
@@ -42,100 +136,12 @@ export default class MonitorLog extends AnalyticsBaseModel {
pluralName: "Monitor Logs",
crudApiPath: new Route("/monitor-log"),
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "monitorId",
title: "Monitor ID",
description: "ID of the monitor which this logs belongs to",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "time",
title: "Time",
description: "When was the log created?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "logBody",
title: "Log Body",
description: "The body of the log",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectMonitor,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectMonitor,
],
update: [],
},
}),
projectIdColumn,
monitorIdColumn,
timeColumn,
logBodyColumn,
],
projections: [],
sortKeys: ["projectId", "time", "monitorId"],
primaryKeys: ["projectId", "time", "monitorId"],
partitionKey: "sipHash64(projectId) % 16",

View File

@@ -41,6 +41,451 @@ export interface SpanLink {
export default class Span extends AnalyticsBaseModel {
public constructor() {
const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const serviceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const startTimeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "startTime",
title: "Start Time",
description: "When did the span start?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const endTimeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "endTime",
title: "End Time",
description: "When did the span end?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const startTimeUnixNanoColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "startTimeUnixNano",
title: "Start Time in Unix Nano",
description: "When did the span start?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const durationUnixNanoColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "durationUnixNano",
title: "Duration in Unix Nano",
description: "How long did the span last?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const endTimeUnixNanoColumn: AnalyticsTableColumn =
new AnalyticsTableColumn({
key: "endTimeUnixNano",
title: "End Time",
description: "When did the span end?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const traceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const spanIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const parentSpanIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "parentSpanId",
title: "Parent Span ID",
description: "ID of the parent span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const traceStateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "traceState",
title: "Trace State",
description: "Trace State",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "attributeKeys",
title: "Attribute Keys",
description: "Attribute keys extracted from attributes",
required: true,
defaultValue: [],
type: TableColumnType.ArrayText,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const eventsColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "events",
title: "Events",
description: "Span Events",
required: true,
defaultValue: [],
type: TableColumnType.JSONArray,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const linksColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "links",
title: "Links",
description: "Span Links",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const statusCodeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "statusCode",
title: "Status Code",
description: "Status Code",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const statusMessageColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "statusMessage",
title: "Status Message",
description: "Status Message",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const nameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "name",
title: "Name",
description: "Name of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
const kindColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
key: "kind",
title: "Kind",
description: "Kind of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
});
super({
tableName: "SpanItem",
tableEngine: AnalyticsTableEngine.MergeTree,
@@ -74,424 +519,27 @@ export default class Span extends AnalyticsBaseModel {
],
},
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "serviceId",
title: "Service ID",
description: "ID of the Service which created the log",
required: true,
type: TableColumnType.ObjectID,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "startTime",
title: "Start Time",
description: "When did the span start?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "endTime",
title: "End Time",
description: "When did the span end?",
required: true,
type: TableColumnType.Date,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "startTimeUnixNano",
title: "Start Time in Unix Nano",
description: "When did the span start?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "durationUnixNano",
title: "Duration in Unix Nano",
description: "How long did the span last?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "endTimeUnixNano",
title: "End Time",
description: "When did the span end?",
required: true,
type: TableColumnType.LongNumber,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "traceId",
title: "Trace ID",
description: "ID of the trace",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "spanId",
title: "Span ID",
description: "ID of the span",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "parentSpanId",
title: "Parent Span ID",
description: "ID of the parent span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "traceState",
title: "Trace State",
description: "Trace State",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "events",
title: "Events",
description: "Span Events",
required: true,
defaultValue: [],
type: TableColumnType.JSONArray,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "links",
title: "Links",
description: "Span Links",
required: true,
defaultValue: {},
type: TableColumnType.JSON,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "statusCode",
title: "Status Code",
description: "Status Code",
required: false,
type: TableColumnType.Number,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "statusMessage",
title: "Status Message",
description: "Status Message",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "name",
title: "Name",
description: "Name of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "kind",
title: "Kind",
description: "Kind of the span",
required: false,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
],
update: [],
},
}),
projectIdColumn,
serviceIdColumn,
startTimeColumn,
endTimeColumn,
startTimeUnixNanoColumn,
durationUnixNanoColumn,
endTimeUnixNanoColumn,
traceIdColumn,
spanIdColumn,
parentSpanIdColumn,
traceStateColumn,
attributesColumn,
attributeKeysColumn,
eventsColumn,
linksColumn,
statusCodeColumn,
statusMessageColumn,
nameColumn,
kindColumn,
],
projections: [],
sortKeys: ["projectId", "startTime", "serviceId", "traceId"],
primaryKeys: ["projectId", "startTime", "serviceId", "traceId"],
partitionKey: "sipHash64(projectId) % 16",
@@ -610,6 +658,14 @@ export default class Span extends AnalyticsBaseModel {
this.setColumnValue("attributes", v);
}
public get attributeKeys(): Array<string> | undefined {
return this.getColumnValue("attributeKeys") as Array<string> | undefined;
}
public set attributeKeys(v: Array<string> | undefined) {
this.setColumnValue("attributeKeys", v);
}
public get events(): Array<SpanEvent> | undefined {
return this.getColumnValue("events") as Array<SpanEvent> | undefined;
}

View File

@@ -1,164 +0,0 @@
import AnalyticsBaseModel from "./AnalyticsBaseModel/AnalyticsBaseModel";
import Route from "../../Types/API/Route";
import AnalyticsTableEngine from "../../Types/AnalyticsDatabase/AnalyticsTableEngine";
import AnalyticsTableColumn from "../../Types/AnalyticsDatabase/TableColumn";
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
import TelemetryType from "../../Types/Telemetry/TelemetryType";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
export default class TelemetryAttribute extends AnalyticsBaseModel {
public constructor() {
super({
tableName: "TelemetryAttribute",
tableEngine: AnalyticsTableEngine.MergeTree,
singularName: "Telemetry Attribute",
pluralName: "Telemetry Attributes",
crudApiPath: new Route("/telemetry-attributes"),
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
Permission.ReadTelemetryServiceLog,
Permission.ReadTelemetryServiceMetrics,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateTelemetryServiceTraces,
Permission.CreateTelemetryServiceLog,
Permission.CreateTelemetryServiceMetrics,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditTelemetryServiceTraces,
Permission.EditTelemetryServiceLog,
Permission.EditTelemetryServiceMetrics,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteTelemetryServiceTraces,
Permission.DeleteTelemetryServiceLog,
Permission.DeleteTelemetryServiceMetrics,
],
},
tableColumns: [
new AnalyticsTableColumn({
key: "projectId",
title: "Project ID",
description: "ID of project",
required: true,
type: TableColumnType.ObjectID,
isTenantId: true,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
Permission.ReadTelemetryServiceLog,
Permission.ReadTelemetryServiceMetrics,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditTelemetryServiceTraces,
Permission.EditTelemetryServiceLog,
Permission.EditTelemetryServiceMetrics,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "telemetryType",
title: "Telemetry Type",
description: "Type of telemetry",
required: true,
type: TableColumnType.Text,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
Permission.ReadTelemetryServiceLog,
Permission.ReadTelemetryServiceMetrics,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditTelemetryServiceTraces,
Permission.EditTelemetryServiceLog,
Permission.EditTelemetryServiceMetrics,
],
update: [],
},
}),
new AnalyticsTableColumn({
key: "attributes",
title: "Attributes",
description: "Attributes",
required: true,
type: TableColumnType.JSONArray,
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadTelemetryServiceTraces,
Permission.ReadTelemetryServiceLog,
Permission.ReadTelemetryServiceMetrics,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditTelemetryServiceTraces,
Permission.EditTelemetryServiceLog,
Permission.EditTelemetryServiceMetrics,
],
update: [],
},
}),
],
sortKeys: ["projectId", "telemetryType"],
primaryKeys: ["projectId", "telemetryType"],
partitionKey: "sipHash64(projectId) % 16",
});
}
public get projectId(): ObjectID | undefined {
return this.getColumnValue("projectId") as ObjectID | undefined;
}
public set projectId(v: ObjectID | undefined) {
this.setColumnValue("projectId", v);
}
public get telemetryType(): TelemetryType | undefined {
return this.getColumnValue("telemetryType") as TelemetryType | undefined;
}
public set telemetryType(v: TelemetryType | undefined) {
this.setColumnValue("telemetryType", v);
}
public get attributes(): Array<string> | undefined {
return this.getColumnValue("attributes") as Array<string> | undefined;
}
public set attributes(v: Array<string> | undefined) {
this.setColumnValue("attributes", v);
}
}

View File

@@ -4,11 +4,13 @@ import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccess
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Route from "../../Types/API/Route";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@TableAccessControl({
@@ -24,6 +26,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
icon: IconProp.Lock,
tableDescription: "HTTP Challege for Lets Encrypt Certificates",
})
@CrudApiEndpoint(new Route("/acme-challenge"))
@Entity({
name: "AcmeChallenge",
})

View File

@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
@@ -418,6 +419,7 @@ export default class AlertFeed extends BaseModel {
],
update: [],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
required: true,

View File

@@ -17,7 +17,16 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import File from "./File";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("alert")
@@ -340,6 +349,54 @@ export default class AlertInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlertInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditAlertInternalNote,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note.",
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "AlertInternalNoteFile",
joinColumn: {
name: "alertInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -378,6 +379,7 @@ export default class AlertSeverity extends BaseModel {
Permission.EditAlertSeverity,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -356,6 +357,7 @@ export default class AlertState extends BaseModel {
Permission.EditAlertState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -15,6 +15,7 @@ import TableColumn, {
getTableColumns,
} from "../../../Types/Database/TableColumn";
import TableColumnType from "../../../Types/Database/TableColumnType";
import { getFirstColorFieldColumn } from "../../../Types/Database/ColorField";
import Dictionary from "../../../Types/Dictionary";
import Email from "../../../Types/Email";
import BadDataException from "../../../Types/Exception/BadDataException";
@@ -123,7 +124,6 @@ export default class DatabaseBaseModel extends BaseEntity {
public isMasterAdminApiDocs!: boolean;
public currentUserCanAccessColumnBy!: string | null;
public labelsColumn!: string | null;
public slugifyColumn!: string | null;
public saveSlugToColumn!: string | null;
public singularName!: string | null;
@@ -204,6 +204,10 @@ export default class DatabaseBaseModel extends BaseEntity {
return new Columns(Object.keys(getTableColumns(this)));
}
public getFirstColorColumn(): string | null {
return getFirstColorFieldColumn(this);
}
public canQueryMultiTenant(): boolean {
return Boolean(this.isMultiTenantRequestAllowed);
}
@@ -345,10 +349,6 @@ export default class DatabaseBaseModel extends BaseEntity {
return this.currentUserCanAccessColumnBy;
}
public getLabelsColumn(): string | null {
return this.labelsColumn;
}
public get id(): ObjectID | null {
return this._id ? new ObjectID(this._id) : null;
}

View File

@@ -301,7 +301,12 @@ export default class Domain extends BaseModel {
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectDomain,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,

View File

@@ -0,0 +1,104 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import { Column, Entity, Index } from "typeorm";
@TableAccessControl({
create: [],
read: [],
update: [],
delete: [],
})
@CrudApiEndpoint(new Route("/enterprise-license"))
@TableMetadata({
tableName: "EnterpriseLicense",
singularName: "Enterprise License",
pluralName: "Enterprise Licenses",
icon: IconProp.Lock,
tableDescription: "Enterprise license keys issued by OneUptime.",
})
@Entity({
name: "EnterpriseLicense",
})
export default class EnterpriseLicense extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Company Name",
description: "Company name associated with this license.",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public companyName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "License Key",
description: "Enterprise license key.",
unique: true,
})
@Index({ unique: true })
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
unique: true,
})
public licenseKey?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.Date,
title: "Expires At",
description: "Expiration date of this license.",
})
@Column({
nullable: false,
type: ColumnType.Date,
})
public expiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.Number,
title: "Annual Contract Value",
description: "Annual contract value (in USD) for this license.",
})
@Column({
nullable: true,
type: ColumnType.Number,
})
public annualContractValue?: number = undefined;
}

View File

@@ -262,6 +262,115 @@ export default class GlobalConfig extends GlobalConfigModel {
})
public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers separated by comma
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Meta WhatsApp Access Token",
description:
"Access token generated from Meta for sending WhatsApp messages.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: true,
})
public metaWhatsAppAccessToken?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Phone Number ID",
description: "The WhatsApp Business phone number ID from Meta.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppPhoneNumberId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Business Account ID",
description: "Business account ID associated with your WhatsApp setup.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppBusinessAccountId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp App ID",
description:
"Facebook App ID used for the WhatsApp Business Platform integration.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppAppId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Meta WhatsApp App Secret",
description: "Facebook App Secret for the WhatsApp Business Platform.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: true,
})
public metaWhatsAppAppSecret?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Webhook Verify Token",
description:
"Verify token configured in Meta to validate webhook subscriptions.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppWebhookVerifyToken?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
@@ -388,4 +497,75 @@ export default class GlobalConfig extends GlobalConfigModel {
transformer: Email.getDatabaseTransformer(),
})
public adminNotificationEmail?: Email = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Enterprise Company Name",
description:
"Company name associated with the validated enterprise license.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public enterpriseCompanyName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Enterprise License Key",
description: "Enterprise license key stored after successful validation.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public enterpriseLicenseKey?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Enterprise License Expires At",
description: "Expiration date of the validated enterprise license.",
})
@Column({
type: ColumnType.Date,
nullable: true,
unique: true,
})
public enterpriseLicenseExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Enterprise License Token",
description: "Signed JWT returned from license validation.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: true,
})
public enterpriseLicenseToken?: string = undefined;
}

View File

@@ -926,6 +926,39 @@ export default class Incident extends BaseModel {
})
public rootCause?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectIncident,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectIncident,
],
})
@TableColumn({
type: TableColumnType.Markdown,
required: false,
isDefaultValueColumn: false,
title: "Postmortem Note",
description: "Document the postmortem summary for this incident.",
})
@Column({
type: ColumnType.Markdown,
nullable: true,
})
public postmortemNote?: string = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -33,6 +33,7 @@ export enum IncidentFeedEventType {
IncidentUpdated = "IncidentUpdated",
RootCause = "RootCause",
RemediationNotes = "RemediationNotes",
PostmortemNote = "PostmortemNote",
OwnerUserRemoved = "OwnerUserRemoved",
OwnerTeamRemoved = "OwnerTeamRemoved",
OnCallPolicy = "OnCallPolicy",

View File

@@ -17,7 +17,16 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import File from "./File";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("incident")
@@ -340,6 +349,54 @@ export default class IncidentInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentInternalNote,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note.",
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "IncidentInternalNoteFile",
joinColumn: {
name: "incidentInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -0,0 +1,353 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
@TableBillingAccessControl({
create: PlanType.Growth,
read: PlanType.Growth,
update: PlanType.Growth,
delete: PlanType.Growth,
})
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteIncidentNoteTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentNoteTemplate,
],
})
@CrudApiEndpoint(new Route("/incident-postmortem-template"))
@Entity({
name: "IncidentPostmortemTemplate",
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@TableMetadata({
tableName: "IncidentPostmortemTemplate",
singularName: "Incident Postmortem Template",
pluralName: "Incident Postmortem Templates",
icon: IconProp.Book,
tableDescription: "Manage postmortem templates for your incidents",
})
export default class IncidentPostmortemTemplate extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentNoteTemplate,
],
})
@Index()
@TableColumn({
type: TableColumnType.Markdown,
title: "Postmortem Note",
description:
"Markdown template used when documenting an incident postmortem.",
})
@Column({
type: ColumnType.Markdown,
nullable: false,
unique: false,
})
public postmortemNote?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentNoteTemplate,
],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Name",
description: "Name of the Postmortem Template",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public templateName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentNoteTemplate,
],
})
@TableColumn({
required: true,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
title: "Template Description",
description: "Description of the Postmortem Template",
})
@Column({
nullable: false,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public templateDescription?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentNoteTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentNoteTemplate,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
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)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -18,7 +18,16 @@ import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import File from "./File";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("incident")
@@ -341,6 +350,54 @@ export default class IncidentPublicNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentPublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentPublicNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentPublicNote,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note.",
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "IncidentPublicNoteFile",
joinColumn: {
name: "incidentPublicNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -378,6 +379,7 @@ export default class IncidentSeverity extends BaseModel {
Permission.EditIncidentSeverity,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -380,6 +381,7 @@ export default class IncidentState extends BaseModel {
Permission.EditIncidentState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -24,6 +24,7 @@ import IncidentFeed from "./IncidentFeed";
import IncidentCustomField from "./IncidentCustomField";
import IncidentInternalNote from "./IncidentInternalNote";
import IncidentNoteTemplate from "./IncidentNoteTemplate";
import IncidentPostmortemTemplate from "./IncidentPostmortemTemplate";
import IncidentOwnerTeam from "./IncidentOwnerTeam";
import IncidentOwnerUser from "./IncidentOwnerUser";
import IncidentPublicNote from "./IncidentPublicNote";
@@ -80,6 +81,7 @@ import ProjectSmtpConfig from "./ProjectSmtpConfig";
//SSO
import ProjectSSO from "./ProjectSso";
import PromoCode from "./PromoCode";
import EnterpriseLicense from "./EnterpriseLicense";
import Reseller from "./Reseller";
import ResellerPlan from "./ResellerPlan";
// ScheduledMaintenances
@@ -100,6 +102,7 @@ import ServiceCopilotCodeRepository from "./ServiceCopilotCodeRepository";
import ShortLink from "./ShortLink";
// SMS
import SmsLog from "./SmsLog";
import WhatsAppLog from "./WhatsAppLog";
import PushNotificationLog from "./PushNotificationLog";
import WorkspaceNotificationLog from "./WorkspaceNotificationLog";
// Status Page
@@ -115,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
import StatusPageOwnerUser from "./StatusPageOwnerUser";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
import StatusPageResource from "./StatusPageResource";
import StatusPageSCIM from "./StatusPageSCIM";
import StatusPageSSO from "./StatusPageSso";
@@ -127,10 +131,12 @@ import TeamComplianceSetting from "./TeamComplianceSetting";
import TelemetryService from "./TelemetryService";
import UsageBilling from "./TelemetryUsageBilling";
import User from "./User";
import UserSession from "./UserSession";
import UserCall from "./UserCall";
// Notification Methods
import UserEmail from "./UserEmail";
import UserPush from "./UserPush";
import UserWhatsApp from "./UserWhatsApp";
// User Notification Rules
import UserNotificationRule from "./UserNotificationRule";
import UserNotificationSetting from "./UserNotificationSetting";
@@ -233,6 +239,7 @@ const AllModelTypes: Array<{
IncidentOwnerUser,
IncidentSeverity,
IncidentNoteTemplate,
IncidentPostmortemTemplate,
AlertState,
Alert,
@@ -261,6 +268,7 @@ const AllModelTypes: Array<{
StatusPageFooterLink,
StatusPageHeaderLink,
StatusPagePrivateUser,
StatusPagePrivateUserSession,
StatusPageHistoryChartBarColorRule,
ScheduledMaintenanceState,
@@ -297,6 +305,7 @@ const AllModelTypes: Array<{
StatusPageOwnerUser,
SmsLog,
WhatsAppLog,
PushNotificationLog,
WorkspaceNotificationLog,
CallLog,
@@ -306,6 +315,7 @@ const AllModelTypes: Array<{
UserSms,
UserCall,
UserPush,
UserWhatsApp,
UserNotificationRule,
UserOnCallLog,
@@ -325,6 +335,7 @@ const AllModelTypes: Array<{
ResellerPlan,
PromoCode,
EnterpriseLicense,
GlobalConfig,
@@ -367,6 +378,7 @@ const AllModelTypes: Array<{
ProbeOwnerTeam,
ProbeOwnerUser,
UserSession,
UserTotpAuth,
UserWebAuthn,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -365,6 +366,7 @@ export default class Label extends AccessControlModel {
Permission.EditProjectLabel,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -379,6 +380,7 @@ export default class MonitorStatus extends BaseModel {
Permission.EditProjectMonitorStatus,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -134,7 +134,7 @@ export default class Project extends TenantModel {
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
@@ -704,8 +704,8 @@ export default class Project extends TenantModel {
type: TableColumnType.Number,
isDefaultValueColumn: true,
required: true,
title: "SMS or Call Current Balance",
description: "Balance in USD for SMS or Call",
title: "SMS, Call, and WhatsApp Current Balance",
description: "Balance in USD for SMS, Call, and WhatsApp",
defaultValue: 0,
})
@Column({
@@ -732,7 +732,7 @@ export default class Project extends TenantModel {
isDefaultValueColumn: true,
required: true,
title: "Auto Recharge Amount",
description: "Auto recharge amount in USD for SMS or Call",
description: "Auto recharge amount in USD for SMS, Call, and WhatsApp",
defaultValue: 20,
})
@Column({
@@ -760,7 +760,7 @@ export default class Project extends TenantModel {
required: true,
title: "Auto Recharge when current balance falls to",
description:
"Auto recharge is triggered when current balance falls to this amount in USD for SMS or Call",
"Auto recharge is triggered when current balance falls to this amount in USD for SMS, Call, and WhatsApp",
defaultValue: 10,
})
@Column({
@@ -798,6 +798,33 @@ export default class Project extends TenantModel {
})
public enableSmsNotifications?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
required: true,
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable WhatsApp Notifications",
description: "Enable WhatsApp notifications for this project.",
defaultValue: false,
})
@Column({
nullable: false,
default: false,
type: ColumnType.Boolean,
})
public enableWhatsAppNotifications?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [
@@ -841,8 +868,9 @@ export default class Project extends TenantModel {
required: true,
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable auto recharge SMS or Call balance",
description: "Enable auto recharge SMS or Call balance for this project.",
title: "Enable auto recharge for SMS, Call, and WhatsApp balance",
description:
"Enable auto recharge for SMS, Call, and WhatsApp balance for this project.",
defaultValue: false,
})
@Column({
@@ -862,8 +890,9 @@ export default class Project extends TenantModel {
isDefaultValueColumn: true,
hideColumnInDocumentation: true,
type: TableColumnType.Boolean,
title: "Low Call and SMS Balance Notification Sent to Owners",
description: "Low Call and SMS Balance Notification Sent to Owners",
title: "Low SMS, Call, and WhatsApp Balance Notification Sent to Owners",
description:
"Low SMS, Call, and WhatsApp Balance Notification Sent to Owners",
defaultValue: false,
})
@Column({
@@ -883,9 +912,10 @@ export default class Project extends TenantModel {
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
hideColumnInDocumentation: true,
title: "Failed Call and SMS Balance Charge Notification Sent to Owners",
title:
"Failed SMS, Call, and WhatsApp Balance Charge Notification Sent to Owners",
description:
"Failed Call and SMS Balance Charge Notification Sent to Owners",
"Failed SMS, Call, and WhatsApp Balance Charge Notification Sent to Owners",
defaultValue: false,
})
@Column({
@@ -906,8 +936,9 @@ export default class Project extends TenantModel {
isDefaultValueColumn: true,
hideColumnInDocumentation: true,
type: TableColumnType.Boolean,
title: "Not Enabled SMS or Call Notification Sent to Owners",
description: "Not Enabled SMS or Call Notification Sent to Owners",
title: "Not Enabled SMS, Call, or WhatsApp Notification Sent to Owners",
description:
"Not Enabled SMS, Call, or WhatsApp Notification Sent to Owners",
defaultValue: false,
})
@Column({

View File

@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
@@ -422,6 +423,7 @@ export default class ScheduledMaintenanceFeed extends BaseModel {
],
update: [],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
required: true,

View File

@@ -16,7 +16,16 @@ import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import File from "./File";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@CanAccessIfCanReadOn("scheduledMaintenance")
@TenantColumn("projectId")
@@ -340,6 +349,54 @@ export default class ScheduledMaintenanceInternalNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenanceInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenanceInternalNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditScheduledMaintenanceInternalNote,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note.",
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "ScheduledMaintenanceInternalNoteFile",
joinColumn: {
name: "scheduledMaintenanceInternalNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -18,7 +18,16 @@ import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import File from "./File";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@EnableDocumentation()
@CanAccessIfCanReadOn("scheduledMaintenance")
@@ -342,6 +351,54 @@ export default class ScheduledMaintenancePublicNote extends BaseModel {
})
public note?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenancePublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenancePublicNote,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditScheduledMaintenancePublicNote,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: File,
title: "Attachments",
description: "Files attached to this note.",
})
@ManyToMany(
() => {
return File;
},
{
eager: false,
},
)
@JoinTable({
name: "ScheduledMaintenancePublicNoteFile",
joinColumn: {
name: "scheduledMaintenancePublicNoteId",
referencedColumnName: "_id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "_id",
},
})
public attachments?: Array<File> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -380,6 +381,7 @@ export default class ScheduledMaintenanceState extends BaseModel {
Permission.EditScheduledMaintenanceState,
],
})
@ColorField()
@TableColumn({
title: "Color",
required: true,

View File

@@ -14,6 +14,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -448,6 +449,7 @@ export default class ServiceCatalog extends BaseModel {
Permission.EditServiceCatalog,
],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
title: "Service Color",

View File

@@ -2331,4 +2331,84 @@ export default class StatusPage extends BaseModel {
create: PlanType.Free,
})
public ipWhitelist?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable Embedded Overall Status Badge",
description:
"Enable embedded overall status badge that can be displayed on external websites?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
nullable: false,
})
@ColumnBillingAccessControl({
read: PlanType.Free,
update: PlanType.Growth,
create: PlanType.Free,
})
public enableEmbeddedOverallStatus?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@Index()
@TableColumn({
type: TableColumnType.ShortText,
required: false,
title: "Embedded Overall Status Token",
description:
"Security token required to access the embedded overall status badge. This token must be provided in the URL.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
@ColumnBillingAccessControl({
read: PlanType.Free,
update: PlanType.Growth,
create: PlanType.Free,
})
public embeddedOverallStatusToken?: string = undefined;
}

View File

@@ -0,0 +1,413 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Project from "./Project";
import StatusPage from "./StatusPage";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import HashedString from "../../Types/HashedString";
import IconProp from "../../Types/Icon/IconProp";
import { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@AllowAccessIfSubscriptionIsUnpaid()
@TableBillingAccessControl({
create: PlanType.Growth,
read: PlanType.Growth,
update: PlanType.Growth,
delete: PlanType.Growth,
})
@CanAccessIfCanReadOn("statusPage")
@TenantColumn("projectId")
@TableAccessControl({
create: [],
read: [],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/status-page-private-user-session"))
@Entity({
name: "StatusPagePrivateUserSession",
})
@TableMetadata({
tableName: "StatusPagePrivateUserSession",
singularName: "Status Page Private User Session",
pluralName: "Status Page Private User Sessions",
icon: IconProp.Lock,
tableDescription:
"Stores status page private user sessions, refresh tokens, and device metadata for secure access control.",
})
export default class StatusPagePrivateUserSession extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Project that owns this private status page session.",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Project ID",
description: "Project identifier for this session.",
required: true,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status page associated with this session.",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Status Page ID",
description: "Identifier for the status page.",
required: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPagePrivateUserId",
type: TableColumnType.Entity,
modelType: StatusPagePrivateUser,
title: "Status Page Private User",
description: "Private user record associated with this session.",
})
@ManyToOne(
() => {
return StatusPagePrivateUser;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "delete",
},
)
@JoinColumn({ name: "statusPagePrivateUserId" })
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
title: "Status Page Private User ID",
description: "Identifier for the status page private user.",
required: true,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPagePrivateUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index({ unique: true })
@TableColumn({
type: TableColumnType.HashedString,
title: "Refresh Token",
description: "Hashed refresh token for the private user session.",
required: true,
hideColumnInDocumentation: true,
})
@Column({
type: ColumnType.HashedString,
length: ColumnLength.HashedString,
nullable: false,
unique: true,
transformer: HashedString.getDatabaseTransformer(),
})
public refreshToken?: HashedString = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Refresh Token Expires At",
description: "Expiration timestamp for the refresh token.",
required: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Active At",
description: "Last time this session was active.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public lastActiveAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Name",
description: "Friendly name for the device used to access the status page.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Type",
description: "Type of device (desktop, mobile, etc).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceType?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device OS",
description: "Operating system reported for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceOS?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Browser",
description: "Browser or client application used for the session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceBrowser?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address recorded for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "User Agent",
description: "User agent string supplied by the client.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Indicates if the session has been revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Revoked At",
description: "Timestamp when the session was revoked, if applicable.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public revokedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Revoked Reason",
description: "Reason provided for revoking this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public revokedReason?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.JSON,
title: "Additional Info",
description: "Flexible JSON payload for storing structured metadata.",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public additionalInfo?: JSONObject = undefined;
}

View File

@@ -8,7 +8,6 @@ import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
@@ -45,12 +44,6 @@ import TelemetryService from "./TelemetryService";
Permission.EditTelemetryException,
],
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@CrudApiEndpoint(new Route("/telemetry-exception-status"))
@TableMetadata({
tableName: "TelemetryException",

View File

@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import ColorField from "../../Types/Database/ColorField";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
@@ -505,6 +506,7 @@ export default class TelemetryService extends BaseModel {
Permission.EditTelemetryService,
],
})
@ColorField()
@TableColumn({
type: TableColumnType.Color,
title: "Service Color",

View File

@@ -39,7 +39,7 @@ export const DEFAULT_RETENTION_IN_DAYS: number = 15;
pluralName: "Telemetry Usage Billings",
icon: IconProp.Billing,
tableDescription:
"Stores historical usage billing data for your telemetry data like Logs, Metrics, and Traces.",
"Stores historical usage billing data for your telemetry data like Logs, Metrics, Traces, and Exceptions.",
})
@Entity({
name: "TelemetryUsageBilling",

View File

@@ -300,21 +300,6 @@ class User extends UserModel {
})
public twoFactorAuthEnabled?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: false,
})
public jwtRefreshToken?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -6,6 +6,7 @@ import UserCall from "./UserCall";
import UserEmail from "./UserEmail";
import UserPush from "./UserPush";
import UserSMS from "./UserSMS";
import UserWhatsApp from "./UserWhatsApp";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -383,6 +384,53 @@ class UserNotificationRule extends BaseModel {
})
public userSmsId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userWhatsAppId",
type: TableColumnType.Entity,
modelType: UserWhatsApp,
title: "User WhatsApp",
description:
"Relation to User WhatsApp Resource in which this object belongs",
})
@ManyToOne(
() => {
return UserWhatsApp;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userWhatsAppId" })
public userWhatsApp?: UserWhatsApp = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User WhatsApp ID",
description: "ID of User WhatsApp in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userWhatsAppId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],

View File

@@ -273,6 +273,22 @@ class UserNotificationSetting extends BaseModel {
})
public alertBySMS?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public alertByWhatsApp?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],

View File

@@ -13,6 +13,7 @@ import UserNotificationRule from "./UserNotificationRule";
import UserPush from "./UserPush";
import UserOnCallLog from "./UserOnCallLog";
import UserSMS from "./UserSMS";
import UserWhatsApp from "./UserWhatsApp";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
@@ -834,6 +835,53 @@ export default class UserOnCallLogTimeline extends BaseModel {
})
public userSmsId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userWhatsAppId",
type: TableColumnType.Entity,
modelType: UserWhatsApp,
title: "User WhatsApp",
description:
"Relation to User WhatsApp Resource in which this object belongs",
})
@ManyToOne(
() => {
return UserWhatsApp;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userWhatsAppId" })
public userWhatsApp?: UserWhatsApp = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User WhatsApp ID",
description: "ID of User WhatsApp in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userWhatsAppId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],

View File

@@ -0,0 +1,318 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import User from "./User";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import HashedString from "../../Types/HashedString";
import IconProp from "../../Types/Icon/IconProp";
import { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation({
isMasterAdminApiDocs: true,
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-session"))
@Entity({
name: "UserSession",
})
@TableMetadata({
tableName: "UserSession",
singularName: "User Session",
pluralName: "User Sessions",
icon: IconProp.Lock,
tableDescription:
"Active user sessions with refresh tokens and device metadata for enhanced authentication security.",
})
@CurrentUserCanAccessRecordBy("userId")
class UserSession extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User account this session belongs to.",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "delete",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
title: "User ID",
description: "Identifier for the user that owns this session.",
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index({ unique: true })
@TableColumn({
type: TableColumnType.HashedString,
title: "Refresh Token",
description: "Hashed refresh token for this session.",
required: true,
hideColumnInDocumentation: true,
})
@Column({
type: ColumnType.HashedString,
length: ColumnLength.HashedString,
nullable: false,
unique: true,
transformer: HashedString.getDatabaseTransformer(),
})
public refreshToken?: HashedString = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Refresh Token Expires At",
description: "Expiration timestamp for the refresh token.",
required: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Active At",
description: "Last time this session was used.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public lastActiveAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Name",
description: "Friendly name for the device used to sign in.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device Type",
description: "Type of device (e.g., desktop, mobile).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceType?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device OS",
description: "Operating system reported for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceOS?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Browser",
description: "Browser or client application used for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public deviceBrowser?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address observed for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "User Agent",
description: "Complete user agent string supplied by the client.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Marks whether the session has been explicitly revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Revoked At",
description: "Timestamp when the session was revoked, if applicable.",
})
@Column({
type: ColumnType.Date,
nullable: true,
})
public revokedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Revoked Reason",
description: "Optional reason describing why the session was revoked.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public revokedReason?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.JSON,
title: "Additional Info",
description:
"Flexible JSON payload for storing structured session metadata.",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public additionalInfo?: JSONObject = undefined;
}
export default UserSession;

View File

@@ -0,0 +1,288 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import Phone from "../../Types/Phone";
import Text from "../../Types/Text";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-whatsapp"))
@Entity({
name: "UserWhatsApp",
})
@TableMetadata({
tableName: "UserWhatsApp",
singularName: "WhatsApp Number",
pluralName: "WhatsApp Numbers",
icon: IconProp.WhatsApp,
tableDescription: "WhatsApp numbers used for WhatsApp notifications.",
})
@CurrentUserCanAccessRecordBy("userId")
class UserWhatsApp extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "WhatsApp Number",
required: true,
unique: false,
type: TableColumnType.Phone,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.Phone,
length: ColumnLength.Phone,
unique: false,
nullable: false,
transformer: Phone.getDatabaseTransformer(),
})
public phone?: Phone = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "user",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "Relation to User who this WhatsApp number belongs to",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "User ID",
description: "User ID who this WhatsApp number belongs to",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
@Index()
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
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)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "Is Verified",
description: "Is this WhatsApp number verified?",
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public isVerified?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
title: "Verification Code",
description: "Temporary Verification Code",
isDefaultValueColumn: true,
computed: true,
required: true,
type: TableColumnType.ShortText,
forceGetDefaultValueOnCreate: () => {
return Text.generateRandomNumber(6);
},
})
@Column({
type: ColumnType.ShortText,
nullable: false,
length: ColumnLength.ShortText,
})
public verificationCode?: string = undefined;
}
export default UserWhatsApp;

View File

@@ -0,0 +1,909 @@
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 ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import Phone from "../../Types/Phone";
import WhatsAppStatus from "../../Types/WhatsAppStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/whatsapp-log"))
@Entity({
name: "WhatsAppLog",
})
@EnableWorkflow({
create: true,
delete: false,
update: false,
})
@TableMetadata({
tableName: "WhatsAppLog",
singularName: "WhatsApp Log",
pluralName: "WhatsApp Logs",
icon: IconProp.WhatsApp,
tableDescription:
"Logs of all the WhatsApp messages sent out to all users and subscribers for this project.",
})
export default class WhatsAppLog extends BaseModel {
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.Phone,
title: "To Number",
description: "Phone Number WhatsApp message was sent to",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.Phone,
length: ColumnLength.Phone,
transformer: Phone.getDatabaseTransformer(),
})
public toNumber?: Phone = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
required: false,
type: TableColumnType.Phone,
title: "From Number",
description:
"Phone Number WhatsApp message was sent from (Business Number ID)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.Phone,
length: ColumnLength.Phone,
transformer: Phone.getDatabaseTransformer(),
})
public fromNumber?: Phone = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.VeryLongText,
title: "Message Text",
description: "Text content of the WhatsApp message",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.VeryLongText,
})
public messageText?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "Status Message",
description: "Status Message (if any)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public statusMessage?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "WhatsApp Message ID",
description: "Message ID returned by Meta's API",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public whatsAppMessageId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Status of the WhatsApp Message",
description: "Status of the WhatsApp message sent",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public status?: WhatsAppStatus = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.Number,
title: "WhatsApp Cost",
description: "WhatsApp Message Cost in USD Cents",
canReadOnRelationQuery: false,
isDefaultValueColumn: true,
defaultValue: 0,
})
@Column({
nullable: false,
default: 0,
type: ColumnType.Number,
})
public whatsAppCostInUSDCents?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "incidentId",
type: TableColumnType.Entity,
modelType: Incident,
title: "Incident",
description: "Incident associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Incident ID",
description: "ID of Incident associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User who initiated this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User ID",
description: "ID of User who initiated this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "alertId",
type: TableColumnType.Entity,
modelType: Alert,
title: "Alert",
description: "Alert associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Alert ID",
description: "ID of Alert associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "scheduledMaintenanceId",
type: TableColumnType.Entity,
modelType: ScheduledMaintenance,
title: "Scheduled Maintenance",
description: "Scheduled Maintenance associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Scheduled Maintenance ID",
description:
"ID of Scheduled Maintenance associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status Page associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page ID",
description: "ID of Status Page associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageAnnouncementId",
type: TableColumnType.Entity,
modelType: StatusPageAnnouncement,
title: "Status Page Announcement",
description:
"Status Page Announcement associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page Announcement ID",
description:
"ID of Status Page Announcement associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "teamId",
type: TableColumnType.Entity,
modelType: Team,
title: "Team",
description: "Team associated with this message (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.ReadWhatsAppLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Team ID",
description: "ID of Team associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public teamId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicy,
title: "On-Call Duty Policy",
description: "On-Call Duty Policy associated with this message (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.ReadWhatsAppLog,
],
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 message (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.ReadWhatsAppLog,
],
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 message (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.ReadWhatsAppLog,
],
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 message (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.ReadWhatsAppLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyScheduleId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicySchedule,
title: "On-Call Duty Policy Schedule",
description:
"On-Call Duty Policy Schedule associated with this message (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.ReadWhatsAppLog,
],
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 message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyScheduleId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
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)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -0,0 +1,64 @@
import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
import NotFoundException from "../../Types/Exception/NotFoundException";
import AcmeChallengeService, {
Service as AcmeChallengeServiceType,
} from "../Services/AcmeChallengeService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
export default class AcmeChallengeAPI extends BaseAPI<
AcmeChallenge,
AcmeChallengeServiceType
> {
private wellKnownRouter: ExpressRouter;
public constructor() {
super(AcmeChallenge, AcmeChallengeService);
this.wellKnownRouter = Express.getRouter();
this.wellKnownRouter.get(
"/acme-challenge/.well-known/:token",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const challenge: AcmeChallenge | null =
await AcmeChallengeService.findOneBy({
query: {
token: req.params["token"] as string,
},
select: {
challenge: true,
},
props: {
isRoot: true,
},
});
if (!challenge) {
return next(new NotFoundException("Challenge not found"));
}
return Response.sendTextResponse(
req,
res,
challenge.challenge as string,
);
} catch (err) {
return next(err);
}
},
);
this.router.use("/", this.wellKnownRouter);
}
public getWellKnownRouter(): ExpressRouter {
return this.wellKnownRouter;
}
}

View File

@@ -0,0 +1,102 @@
import EnterpriseLicense from "../../Models/DatabaseModels/EnterpriseLicense";
import BadDataException from "../../Types/Exception/BadDataException";
import { JSONObject } from "../../Types/JSON";
import EnterpriseLicenseService, {
Service as EnterpriseLicenseServiceType,
} from "../Services/EnterpriseLicenseService";
import UserMiddleware from "../Middleware/UserAuthorization";
import JSONWebToken from "../Utils/JsonWebToken";
import Response from "../Utils/Response";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import BaseAPI from "./BaseAPI";
// import { Host } from "../EnvironmentConfig";
export default class EnterpriseLicenseAPI extends BaseAPI<
EnterpriseLicense,
EnterpriseLicenseServiceType
> {
public constructor() {
super(EnterpriseLicense, EnterpriseLicenseService);
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/validate`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const licenseKey: string | undefined = req.body["licenseKey"];
if (!licenseKey) {
throw new BadDataException("License key is required");
}
//const serverHost: string = Host.toString();
/*
* if (!serverHost.includes("oneuptime.com")) {
* throw new BadDataException(
* "Enterprise license validation is only available on oneuptime.com",
* );
* }
*/
const license: EnterpriseLicense | null =
await EnterpriseLicenseService.findOneBy({
query: {
licenseKey: licenseKey,
},
select: {
companyName: true,
expiresAt: true,
licenseKey: true,
},
props: {
isRoot: true,
},
});
if (!license) {
throw new BadDataException("License key is invalid");
}
if (!license.expiresAt) {
throw new BadDataException("License expiration is not set");
}
const now: number = Date.now();
const expiresAtMs: number = license.expiresAt.getTime();
const secondsUntilExpiry: number = Math.floor(
(expiresAtMs - now) / 1000,
);
if (secondsUntilExpiry <= 0) {
throw new BadDataException("License key has expired");
}
const payload: JSONObject = {
companyName: license.companyName || "",
expiresAt: license.expiresAt.toISOString(),
licenseKey: license.licenseKey || "",
};
const token: string = JSONWebToken.signJsonPayload(
payload,
Math.max(secondsUntilExpiry, 1),
);
return Response.sendJsonObjectResponse(req, res, {
companyName: payload["companyName"] as string,
expiresAt: payload["expiresAt"] as string,
licenseKey: payload["licenseKey"] as string,
token,
});
} catch (err) {
next(err);
}
},
);
}
}

View File

@@ -9,6 +9,15 @@ import {
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import GlobalConfig from "../../Models/DatabaseModels/GlobalConfig";
import ObjectID from "../../Types/ObjectID";
import { JSONObject } from "../../Types/JSON";
import BadDataException from "../../Types/Exception/BadDataException";
import API from "../../Utils/API";
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
import HTTPResponse from "../../Types/API/HTTPResponse";
import PartialEntity from "../../Types/Database/PartialEntity";
import { EnterpriseLicenseValidationUrl } from "../EnvironmentConfig";
import UserMiddleware from "../Middleware/UserAuthorization";
export default class GlobalConfigAPI extends BaseAPI<
GlobalConfig,
@@ -45,5 +54,164 @@ export default class GlobalConfigAPI extends BaseAPI<
}
},
);
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/license`,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const config: GlobalConfig | null =
await GlobalConfigService.findOneById({
id: ObjectID.getZeroObjectID(),
select: {
enterpriseCompanyName: true,
enterpriseLicenseExpiresAt: true,
enterpriseLicenseKey: true,
enterpriseLicenseToken: true,
},
props: {
isRoot: true,
},
});
const responseBody: JSONObject = {
companyName: config?.enterpriseCompanyName || null,
expiresAt: config?.enterpriseLicenseExpiresAt
? config.enterpriseLicenseExpiresAt.toISOString()
: null,
licenseKey: config?.enterpriseLicenseKey || null,
token: config?.enterpriseLicenseToken || null,
};
return Response.sendJsonObjectResponse(req, res, responseBody);
} catch (err) {
next(err);
}
},
);
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/license`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const licenseKey: string =
(req.body["licenseKey"] as string | undefined)?.trim() || "";
if (!licenseKey) {
throw new BadDataException("License key is required");
}
const validationResponse:
| HTTPResponse<JSONObject>
| HTTPErrorResponse = await API.post<JSONObject>({
url: EnterpriseLicenseValidationUrl,
data: {
licenseKey,
},
});
if (!validationResponse.isSuccess()) {
const errorMessage: string =
validationResponse instanceof HTTPErrorResponse
? validationResponse.message ||
"Failed to validate license key."
: "Failed to validate license key.";
throw new BadDataException(errorMessage);
}
const payload: JSONObject = validationResponse.data as JSONObject;
const companyNameRaw: string =
(payload["companyName"] as string | undefined)?.trim() || "";
const expiresAtRaw: string =
(payload["expiresAt"] as string | undefined) || "";
const licenseKeyRaw: string =
(payload["licenseKey"] as string | undefined)?.trim() || licenseKey;
const licenseToken: string =
(payload["token"] as string | undefined) || "";
let licenseExpiry: Date | undefined = undefined;
if (expiresAtRaw) {
const parsedDate: Date = new Date(expiresAtRaw);
if (Number.isNaN(parsedDate.getTime())) {
throw new BadDataException(
"License expiration returned from server is invalid.",
);
}
licenseExpiry = parsedDate;
}
const updatePayload: PartialEntity<GlobalConfig> = {
enterpriseCompanyName: companyNameRaw || null,
enterpriseLicenseKey: licenseKeyRaw || null,
enterpriseLicenseExpiresAt: licenseExpiry || null,
enterpriseLicenseToken: licenseToken || null,
};
const globalConfigId: ObjectID = ObjectID.getZeroObjectID();
const existingConfig: GlobalConfig | null =
await GlobalConfigService.findOneById({
id: globalConfigId,
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (existingConfig) {
await GlobalConfigService.updateOneById({
id: globalConfigId,
data: updatePayload,
props: {
isRoot: true,
ignoreHooks: true,
},
});
} else {
const newConfig: GlobalConfig = new GlobalConfig();
newConfig.id = globalConfigId;
if (companyNameRaw) {
newConfig.enterpriseCompanyName = companyNameRaw;
}
if (licenseKeyRaw) {
newConfig.enterpriseLicenseKey = licenseKeyRaw;
}
if (licenseToken) {
newConfig.enterpriseLicenseToken = licenseToken;
}
if (licenseExpiry) {
newConfig.enterpriseLicenseExpiresAt = licenseExpiry;
}
await GlobalConfigService.create({
data: newConfig,
props: {
isRoot: true,
ignoreHooks: true,
},
});
}
return Response.sendJsonObjectResponse(req, res, {
companyName: companyNameRaw || null,
expiresAt: licenseExpiry ? licenseExpiry.toISOString() : null,
licenseKey: licenseKeyRaw || null,
token: licenseToken || null,
});
} catch (err) {
next(err);
}
},
);
}
}

View File

@@ -58,22 +58,31 @@ export default class MicrosoftTeamsAPI {
"https://developer.microsoft.com/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
manifestVersion: "1.23",
version: AppVersion.toLowerCase().includes("unknown")
? "1.1.0"
? "1.5.0"
: AppVersion,
id: MicrosoftTeamsAppClientId,
developer: {
name: "OneUptime",
name: "HackerBay Inc",
websiteUrl: "https://oneuptime.com",
privacyUrl: "https://oneuptime.com/legal/privacy",
termsOfUseUrl: "https://oneuptime.com/legal/terms",
},
publisherDocsUrl:
"https://oneuptime.com/docs/workspace-connections/microsoft-teams",
name: {
short: "OneUptime",
full: "OneUptime - Complete Observability Platform",
},
description: {
short: "Monitor your apps, websites, APIs, and more with OneUptime",
full: "OneUptime is a complete open-source observability platform that helps you monitor your applications, websites, APIs, and infrastructure. Get alerted when things go wrong and maintain your SLAs.",
short: "Complete open-source monitoring and observability platform. ",
full: `<p>OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors.</p>
<p>In order to use the app, you need to have an active account with <a href="https://oneuptime.com" target="_blank">OneUptime</a>. Please send an email to <a href="mailto:support@oneuptime.com">support@oneuptime.com</a> if you need more details.</p>
<p><strong>Create a new OneUptime Account:</strong> If you wish to sign up for a new account, you can do so by visiting <a href="https://oneuptime.com" target="_blank">OneUptime Sign Up</a>.</p>
<p><strong>Help and Support:</strong> You can reach out to help and support via <a href="https://oneuptime.com/support" target="_blank">Support Page</a> or contact <a href="mailto:support@oneuptime.com">support@oneuptime.com</a>.</p>
`,
},
// Default to size-specific names; route will adjust if fallbacks are used
icons: {
@@ -614,9 +623,8 @@ export default class MicrosoftTeamsAPI {
projectId: new ObjectID(projectId),
workspaceType: WorkspaceType.MicrosoftTeams,
});
const existingTenant: string | undefined = (
existingAuth?.miscData as any
)?.tenantId;
const existingTenant: string | undefined =
existingAuth?.workspaceProjectId;
if (existingTenant) {
tenantForConsent = existingTenant;
}

View File

@@ -4,11 +4,11 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BadDataException from "../../Types/Exception/BadDataException";
import Exception from "../../Types/Exception/Exception";
import JSONFunctions from "../../Types/JSONFunctions";
import ObjectID from "../../Types/ObjectID";
import Permission, { UserPermission } from "../../Types/Permission";
@@ -19,7 +19,7 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/notification/recharge",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
let amount: number | PositiveNumber = JSONFunctions.deserializeValue(
req.body.amount,
@@ -106,8 +106,8 @@ router.post(
),
);
}
} catch (err: any) {
return Response.sendErrorResponse(req, res, err as Exception);
} catch (err) {
return next(err);
}
return Response.sendEmptySuccessResponse(req, res);

View File

@@ -20,11 +20,72 @@ import PositiveNumber from "../../Types/PositiveNumber";
import Project from "../../Models/DatabaseModels/Project";
import Reseller from "../../Models/DatabaseModels/Reseller";
import TeamMember from "../../Models/DatabaseModels/TeamMember";
import BadDataException from "../../Types/Exception/BadDataException";
import Permission, { UserPermission } from "../../Types/Permission";
import ObjectID from "../../Types/ObjectID";
import { JSONObject } from "../../Types/JSON";
export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
public constructor() {
super(Project, ProjectService);
this.router.put(
`${new this.entityType().getCrudApiPath()?.toString()}/:id/change-plan`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
if (!IsBillingEnabled) {
throw new BadDataException(
"Billing is not enabled for this server",
);
}
const projectId: ObjectID = new ObjectID(req.params["id"] as string);
const body: JSONObject = (req.body as JSONObject) || {};
const data: JSONObject = (body["data"] as JSONObject) || {};
const paymentProviderPlanId: string | undefined = data[
"paymentProviderPlanId"
] as string | undefined;
if (!paymentProviderPlanId) {
throw new BadDataException("Plan ID is required to change plan");
}
const permissions: Array<UserPermission> =
await this.getPermissionsForTenant(req);
const hasBillingPermission: boolean =
permissions.filter((permission: UserPermission) => {
return (
permission.permission.toString() ===
Permission.ProjectOwner.toString() ||
permission.permission.toString() ===
Permission.ManageProjectBilling.toString()
);
}).length > 0;
if (
!hasBillingPermission &&
!(req as OneUptimeRequest).userAuthorization?.isMasterAdmin
) {
throw new BadDataException(
`You need ${Permission.ProjectOwner} or ${Permission.ManageProjectBilling} permission to change project plan`,
);
}
await ProjectService.changePlan({
projectId: projectId,
paymentProviderPlanId: paymentProviderPlanId,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
next(err);
}
},
);
/*
* This API lists all the projects where user is its team member.
* This API is usually used to show project selector dropdown in the UI

View File

@@ -1,7 +1,11 @@
import ProjectSsoService, {
Service as ProjectSsoServiceType,
} from "../Services/ProjectSsoService";
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -22,43 +26,47 @@ export default class ProjectSsoAPI extends BaseAPI<
`${new this.entityType()
.getCrudApiPath()
?.toString()}/:projectId/sso-list`,
async (req: ExpressRequest, res: ExpressResponse) => {
const projectId: ObjectID = new ObjectID(
req.params["projectId"] as string,
);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const projectId: ObjectID = new ObjectID(
req.params["projectId"] as string,
);
if (!projectId) {
return Response.sendErrorResponse(
if (!projectId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid project id."),
);
}
const sso: Array<ProjectSSO> = await this.service.findBy({
query: {
projectId: projectId,
isEnabled: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
name: true,
description: true,
_id: true,
},
props: {
isRoot: true,
},
});
return Response.sendEntityArrayResponse(
req,
res,
new BadDataException("Invalid project id."),
sso,
new PositiveNumber(sso.length),
ProjectSSO,
);
} catch (err) {
return next(err);
}
const sso: Array<ProjectSSO> = await this.service.findBy({
query: {
projectId: projectId,
isEnabled: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
name: true,
description: true,
_id: true,
},
props: {
isRoot: true,
},
});
return Response.sendEntityArrayResponse(
req,
res,
sso,
new PositiveNumber(sso.length),
ProjectSSO,
);
},
);
}

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