Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions App/FeatureSet/Docs/Content/en/installation/docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ sudo bash -c "(export $(grep -v '^#' config.env | xargs) && docker compose up --

OneUptime should run at: http://localhost. You need to register a new account for your instance to start using it.

### Configuring HOST and port

The `HOST` variable in `config.env` must be the **public IP address or domain name** that browsers can reach your server at. Do **not** set it to `localhost` or `127.0.0.1` — those resolve to the container itself inside Docker and will break generated links in emails, status pages, and notifications.

```bash
# Good — reachable IP or domain
HOST=192.168.1.100 # LAN setup
HOST=oneuptime.example.com # internet-facing

# Bad — will break links inside Docker
HOST=localhost
```

To run OneUptime on a port other than 80, change `ONEUPTIME_HTTP_PORT`:

```bash
ONEUPTIME_HTTP_PORT=4000 # access at http://<HOST>:4000
```

If you change `ONEUPTIME_HTTP_PORT`, also update the probe URLs so they can reach the API:

```bash
GLOBAL_PROBE_1_ONEUPTIME_URL=http://localhost:4000
GLOBAL_PROBE_2_ONEUPTIME_URL=http://localhost:4000
```

### Setting up TLS/SSL Certificates

OneUptime **does not** support setting up SSL/TLS certificates. You need to set up SSL/TLS certificates on your own.
Expand All @@ -71,6 +97,35 @@ If you need to use SSL/TLS certificates, follow these steps:
- Set `HTTP_PROTOCOL` env var to `https`.
- Change `HOST` env var to the domain name of the server where the reverse proxy is hosted.

### Using Cloudflare Tunnel

Cloudflare Tunnel is a popular zero-config alternative to an nginx reverse proxy. Because Cloudflare handles TLS termination, configure OneUptime as follows:

```bash
# Cloudflare terminates TLS — OneUptime runs plain HTTP
HTTP_PROTOCOL=http
PROVISION_SSL=false

# Your primary OneUptime domain routed through the tunnel
HOST=oneuptime.example.com
ONEUPTIME_HTTP_PORT=8013 # any free port on your host

# Probe URLs must reach the local port directly
GLOBAL_PROBE_1_ONEUPTIME_URL=http://localhost:8013
GLOBAL_PROBE_2_ONEUPTIME_URL=http://localhost:8013
```

In your Cloudflare Zero Trust dashboard, create two public hostnames:

| Public hostname | Service |
|---|---|
| `oneuptime.example.com` | `http://localhost:8013` |
| `status-origin.example.com` | `http://localhost:8014` |

For custom-domain status pages (e.g. `status.customer.com`), point them to `status-origin.example.com` rather than directly to the tunnel. Set `STATUS_PAGE_CNAME_RECORD=status-origin.example.com` in `config.env`.

> **Note:** Cloudflare CNAME flattening prevents other domains from creating a CNAME to a Cloudflare Tunnel hostname. Use the [Cloudflare Custom Hostnames](https://developers.cloudflare.com/ssl/ssl-tls/custom-hostnames/) feature if you need arbitrary domains to point at your OneUptime status page.

## Production Readiness Checklist

Ideally do not deploy OneUptime in production with docker-compose. We highly recommend using Kubernetes. There's a helm chart available for OneUptime [here](https://artifacthub.io/packages/helm/oneuptime/oneuptime).
Expand Down
63 changes: 47 additions & 16 deletions Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import AlertStateService from "../../../Services/AlertStateService";
// Import user services
import User from "../../../../Models/DatabaseModels/User";
import UserService from "../../../Services/UserService";
import WorkspaceUserAuthToken from "../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
import WorkspaceUserAuthTokenService from "../../../Services/WorkspaceUserAuthTokenService";

// Import database utilities
import QueryHelper from "../../../Types/Database/QueryHelper";
Expand Down Expand Up @@ -3215,7 +3217,7 @@ All monitoring checks are passing normally.`;
}
}

// Method to get user's joined teams using app-scoped token
// Method to get user's joined teams, preferring user-scoped delegated token
@CaptureSpan()
public static async getUserJoinedTeams(data: {
userId: ObjectID;
Expand All @@ -3225,11 +3227,44 @@ All monitoring checks are passing normally.`;
projectId: data.projectId.toString(),
userId: data.userId.toString(),
});
logger.debug(`User ID: ${data.userId.toString()}`);
logger.debug(`Project ID: ${data.projectId.toString()}`);

try {
// Fetch user email from UserService
// Prefer user-scoped delegated token so we only need Team.ReadBasic.All delegated permission
const userAuth: WorkspaceUserAuthToken | null =
await WorkspaceUserAuthTokenService.getUserAuth({
projectId: data.projectId,
userId: data.userId,
workspaceType: WorkspaceType.MicrosoftTeams,
});

if (userAuth?.authToken) {
logger.debug(
"Using user-scoped delegated token to fetch joined teams via /me/joinedTeams",
);
const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>({
url: URL.fromString(
"https://graph.microsoft.com/v1.0/me/joinedTeams",
),
headers: {
Authorization: `Bearer ${userAuth.authToken}`,
},
});

if (teamsResponse instanceof HTTPErrorResponse) {
logger.warn(
"User-scoped token request failed, will fall back to app token:",
);
logger.warn(teamsResponse);
} else {
const teams: Array<JSONObject> =
(teamsResponse.data["value"] as Array<JSONObject>) || [];
logger.debug(`Fetched ${teams.length} joined teams via user scope`);
return teams;
}
}

// Fall back to app-scoped token with users/{email}/joinedTeams
const user: User | null = await UserService.findOneById({
id: data.userId,
select: {
Expand All @@ -3240,23 +3275,20 @@ All monitoring checks are passing normally.`;
},
});
if (!user || !user.email) {
logger.error("User or user email not found");
throw new BadDataException(
"User email not found for Microsoft Teams integration",
);
}
const userEmail: string = user.email.toString();
logger.debug(`Retrieved user email: ${userEmail}`);

// Get a valid app access token (refreshed if needed)
logger.debug("Refreshing app access token before fetching teams");
logger.debug(
"Falling back to app-scoped token for users/{email}/joinedTeams",
);
const accessToken: string = await this.getValidAccessToken({
authToken: "", // Not needed for app token refresh
authToken: "",
projectId: data.projectId,
});
logger.debug("App access token refreshed successfully");

// Get user's teams using app-scoped token
const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>({
url: URL.fromString(
Expand All @@ -3273,12 +3305,11 @@ All monitoring checks are passing normally.`;
throw teamsResponse;
}

const teamsData: JSONObject = teamsResponse.data;
const teams: Array<JSONObject> =
(teamsData["value"] as Array<JSONObject>) || [];

logger.debug(`Fetched ${teams.length} joined teams`);

(teamsResponse.data["value"] as Array<JSONObject>) || [];
logger.debug(
`Fetched ${teams.length} joined teams via app-scoped fallback`,
);
return teams;
} catch (error) {
logger.error("Error getting user joined teams:", {
Expand Down
2 changes: 1 addition & 1 deletion Common/Types/Workflow/Components/JavaScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const components: Array<ComponentMetadata> = [
type: ComponentInputType.JSON,
name: "Arguments",
description:
"Pass in arguments to your JavaScript Code from this workflow",
"Pass data into your JavaScript code. Use {{local.components.COMPONENT_ID.returnValue}} to reference the output of a previous component (replace COMPONENT_ID with the ID shown on that component). Inside your script, arguments are available as the 'args' variable.",
required: false,
id: "arguments",
},
Expand Down
6 changes: 3 additions & 3 deletions Common/Types/Workflow/Components/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ const components: Array<ComponentMetadata> = [
title: "Execute Workflow",
category: "Utils",
description:
"Execute another workflow in the same project (fire-and-forget)",
"Execute another workflow in the same project (fire-and-forget). To use this: 1) Create a separate workflow with a 'Manual' trigger. 2) Copy its Trigger ID from the workflow settings. 3) Select that workflow in the 'Workflow' dropdown below.",
iconProp: IconProp.Workflow,
componentType: ComponentType.Component,
arguments: [
{
type: ComponentInputType.WorkflowSelect,
name: "Workflow",
description:
"Pick the workflow to execute. The workflow must be in the same project and be enabled. It must have a Manual trigger to receive the arguments passed below.",
"Pick the workflow to execute. The target workflow must be in the same project, be enabled, and have a Manual trigger. Select it from the dropdown — all eligible workflows in this project are listed.",
required: true,
id: "workflowId",
placeholder: "Select a workflow",
Expand All @@ -28,7 +28,7 @@ const components: Array<ComponentMetadata> = [
type: ComponentInputType.JSON,
name: "Arguments",
description:
"JSON payload to pass to the target workflow. The target workflow's Manual trigger will emit this object on its output port.",
"JSON payload to pass to the target workflow. The target workflow's Manual trigger will emit this object on its output port. Use {{local.components.COMPONENT_ID.returnValue}} to include output from previous components.",
required: false,
id: "arguments",
placeholder: '{ "key": "value" }',
Expand Down
31 changes: 30 additions & 1 deletion Common/UI/Components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Color from "../../../Types/Color";
import OneUptimeDate from "../../../Types/Date";
import StartAndEndTime from "../../../Types/Time/StartAndEndTime";
import moment from "moment-timezone";
import React, { FunctionComponent, ReactElement, useMemo } from "react";
import React, { FunctionComponent, ReactElement, useEffect, useMemo } from "react";
import {
Calendar,
DateLocalizer,
Expand Down Expand Up @@ -42,6 +42,35 @@ const CalendarElement: FunctionComponent<ComponentProps> = (
};
}, []);

// react-big-calendar does not fire onRangeChange on the initial render.
// Compute the initial range here so callers can fetch events immediately.
useEffect(() => {
const view: DefaultCalendarView =
props.defaultCalendarView || DefaultCalendarView.Week;
const now: Date = defaultDate;

let startTime: Date;
let endTime: Date;

if (view === DefaultCalendarView.Week) {
startTime = moment(now).startOf("week").toDate();
endTime = moment(now).endOf("week").toDate();
} else if (view === DefaultCalendarView.Month) {
startTime = moment(now).startOf("month").toDate();
endTime = moment(now).endOf("month").toDate();
} else if (view === DefaultCalendarView.Day) {
startTime = moment(now).startOf("day").toDate();
endTime = moment(now).endOf("day").toDate();
} else {
// Agenda: 30-day window
startTime = moment(now).startOf("day").toDate();
endTime = moment(now).add(30, "days").endOf("day").toDate();
}

props.onRangeChange({ startTime, endTime });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const eventStyleGetter: EventPropGetter<any> = (
event: CalendarEvent,
): { className?: string | undefined; style?: React.CSSProperties } => {
Expand Down
15 changes: 12 additions & 3 deletions config.example.env
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
#!/usr/bin/env bash

# Please change this to domain of the server where oneuptime is hosted on.
HOST=localhost
# The hostname or IP address of the server where OneUptime is hosted.
# IMPORTANT: Do NOT use "localhost" or "127.0.0.1" here.
# - Inside Docker containers, "localhost" resolves to the container itself, not the host machine.
# - OneUptime uses HOST to generate URLs for emails, status pages, and links.
# If HOST is "localhost", those links will be unreachable by external users and by other containers.
# Use the server's public IP address (e.g. HOST=192.168.1.100) for LAN setups,
# or a fully qualified domain name (e.g. HOST=oneuptime.yourcompany.com) for internet-facing deployments.
HOST=oneuptime.yourcompany.com
PROVISION_SSL=false

# OneUptime Port. This is the port where OneUptime will be hosted on.
# The HTTP port that the OneUptime nginx proxy will listen on.
# Change this if port 80 is already in use on your host.
# For example, set ONEUPTIME_HTTP_PORT=4000 to access OneUptime at http://<HOST>:4000
# Note: Do NOT change the PORT variable inside individual service sections; only change this one.
ONEUPTIME_HTTP_PORT=80

# ==============================================
Expand Down
Loading