mirror of
https://github.com/rajnandan1/kener.git
synced 2026-04-28 14:29:56 -05:00
Merge pull request #348 from rajnandan1/rbac
Features role-based access control and user management
This commit is contained in:
@@ -43,6 +43,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
print_width = 180
|
||||
|
||||
# Dockerfile
|
||||
[Dockerfile*]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Create Incident Template
|
||||
about: Create Incident Template
|
||||
title: Title of Incident
|
||||
labels: incident
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Your Incident Description goes here. Markdown Supported
|
||||
|
||||
[start_datetime:utcSeconds]
|
||||
[end_datetime:utcSeconds]
|
||||
+1
-1
@@ -54,7 +54,7 @@
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
"printWidth": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
-16
@@ -121,22 +121,6 @@ COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
|
||||
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Install libcap tools before setting capabilities
|
||||
RUN if [ -f "/etc/alpine-release" ]; then \
|
||||
apk add --no-cache libcap; \
|
||||
else \
|
||||
apt-get update && apt-get install -y libcap2-bin && rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Set capabilities for ping (before changing to non-root user)
|
||||
RUN if [ -f "/etc/alpine-release" ]; then \
|
||||
# Alpine path
|
||||
setcap cap_net_raw+ep /bin/ping; \
|
||||
else \
|
||||
# Debian path
|
||||
setcap cap_net_raw+ep /usr/bin/ping; \
|
||||
fi
|
||||
|
||||
# Ensure necessary directories are writable
|
||||
VOLUME ["/uploads", "/database"]
|
||||
|
||||
|
||||
@@ -7,6 +7,46 @@ description: Changelogs for Kener
|
||||
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## v3.2.5
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- Added code editing for all JavaScript and JSON fields for trigger custom body and monitor `evals`.
|
||||
- Syntax highlighting and error checking for monitor evaluation functions
|
||||
- Added support for custom Discord and Slack body templates in triggers
|
||||
- Improved variable templating for all notification types
|
||||
- Added comprehensive user management with 3 roles: admin, editor, and member. Read more [here](/docs/rbac)
|
||||
- Self-service profile management for all users
|
||||
- User activation/deactivation controls for admins
|
||||
- Added dedicated Badges management page in the admin dashboard
|
||||
- Site-wide status badges to show overall service health
|
||||
- Version information automatically pulled from package.json
|
||||
|
||||
### Improvements
|
||||
|
||||
- Improved build time by migrating lucid-svelte to individual components
|
||||
|
||||
### Github Issues Resolved
|
||||
|
||||
- Feature: Enhancement of Webhook Custom Body Functionality [#238](https://github.com/rajnandan1/kener/issues/238)
|
||||
- Feature: Add LDAP for authentication [#210](https://github.com/rajnandan1/kener/issues/210)
|
||||
- the event created by tigger can't update affects. [#309](https://github.com/rajnandan1/kener/issues/309)
|
||||
- Multiple account or RBAC [#334](https://github.com/rajnandan1/kener/issues/334)
|
||||
- The event that has been edited isn't clickable after you save it [#350](https://github.com/rajnandan1/kener/issues/350)
|
||||
- /app/package.json ERROR on fresh install [#346](https://github.com/rajnandan1/kener/issues/346)
|
||||
- Site stops when monitor processing throws an error [#345](https://github.com/rajnandan1/kener/issues/345)
|
||||
- Support for Self Signed Certificates [#351](https://github.com/rajnandan1/kener/issues/351)
|
||||
- Wrong month is showing up after clicking on the 'Browse Events' link [#349](https://github.com/rajnandan1/kener/issues/349)
|
||||
|
||||
### Migration Notes
|
||||
|
||||
This update is fully backward compatible with existing installations. User accounts created before this update will retain admin privileges by default.
|
||||
|
||||
## v3.2.1
|
||||
|
||||
<picture>
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Kener Role Based Access Control (RBAC)
|
||||
description: Role-Based Access Control (RBAC) in Kener
|
||||
---
|
||||
|
||||
# Role-Based Access Control (RBAC) in Kener
|
||||
|
||||
Kener includes a comprehensive role-based access control system that allows you to manage user permissions and access to various features. This document explains the available roles, their permissions, and how to manage users effectively.
|
||||
|
||||
## Available Roles
|
||||
|
||||
Kener offers three different roles with varying levels of permissions:
|
||||
|
||||
| Role | Description |
|
||||
| ------ | ------------------------------------------------------------------------------ |
|
||||
| Admin | Full access to all features including user management and system configuration |
|
||||
| Editor | Can create and edit monitors, triggers, incidents, and other operational data |
|
||||
| Member | Read-only access with limited interaction capabilities |
|
||||
|
||||
## Role Permissions
|
||||
|
||||
### Admin
|
||||
|
||||
Admins have unrestricted access to the entire system:
|
||||
|
||||
- **User Management**:
|
||||
|
||||
- Create, update, and deactivate users
|
||||
- Change user roles
|
||||
- Reset passwords for other users
|
||||
- Send verification emails
|
||||
|
||||
- **System Configuration**:
|
||||
|
||||
- Configure all site settings
|
||||
- Manage API keys
|
||||
- Set up triggers and integrations
|
||||
|
||||
- **Operational Access**:
|
||||
- Full access to create and manage monitors
|
||||
- Create and update incidents
|
||||
- Run tests on monitors and triggers
|
||||
- Configure all notification channels
|
||||
|
||||
### Editor
|
||||
|
||||
Editors can manage most operational aspects but cannot administer users:
|
||||
|
||||
- **Content Management**:
|
||||
|
||||
- Create and edit monitors
|
||||
- Configure monitor settings
|
||||
- Create and manage triggers
|
||||
- Create and update incidents
|
||||
- Add incident updates and comments
|
||||
|
||||
- **Limited Access**:
|
||||
- Cannot manage users
|
||||
- Cannot change role assignments
|
||||
- Cannot access certain system-level configurations
|
||||
|
||||
### Member
|
||||
|
||||
Members have read-only access with minimal interaction capabilities:
|
||||
|
||||
- **View Access**:
|
||||
|
||||
- View all monitors and their status
|
||||
- View incidents and their history
|
||||
- See system configuration (but cannot modify)
|
||||
|
||||
- **Limited Interactions**:
|
||||
- Can test existing triggers but cannot create or edit them
|
||||
- Cannot create or update incidents
|
||||
- Cannot modify any system configuration
|
||||
|
||||
## Managing Users
|
||||
|
||||
### Adding New Users
|
||||
|
||||
Only Admins and Editors can add new users to the system:
|
||||
|
||||
1. Navigate to the **Users** page in the management dashboard
|
||||
2. Click the **Add New User** button
|
||||
3. Fill in the required information:
|
||||
- Name
|
||||
- Email
|
||||
- Password
|
||||
- Role (Member or Editor)
|
||||
4. Click **Add User** to create the account
|
||||
|
||||
When a new user is added, a verification email can be sent to confirm their email address if email sending is configured.
|
||||
|
||||
### User Settings
|
||||
|
||||
Admins can manage user accounts through the user settings page:
|
||||
|
||||
- **Email Verification**: Send verification emails to users
|
||||
- **Password Reset**: Reset a user's password
|
||||
- **Role Management**: Change a user's role between Member and Editor
|
||||
- **Account Status**: Activate or deactivate user accounts
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Follow the Principle of Least Privilege**:
|
||||
|
||||
- Assign the minimum necessary permissions for users to perform their job
|
||||
- Start users with the Member role and elevate as needed
|
||||
|
||||
2. **Regular Access Reviews**:
|
||||
|
||||
- Periodically review user access and roles
|
||||
- Remove access for users who no longer need it
|
||||
|
||||
3. **Admin Accounts**:
|
||||
- Limit the number of Admin accounts
|
||||
- Use strong passwords for Admin accounts
|
||||
- Consider using email verification for all users
|
||||
|
||||
## Role Limitations
|
||||
|
||||
- The Admin role can only be assigned during initial setup or by existing Admins
|
||||
- Members cannot create or modify content
|
||||
- Users cannot modify their own role (only an Admin can change roles)
|
||||
|
||||
## Email Verification
|
||||
|
||||
When email sending is configured, users can verify their email addresses:
|
||||
|
||||
1. Admins can send verification emails from the user management interface
|
||||
2. Users receive an email with a verification link
|
||||
3. After clicking the link, the user's email is marked as verified
|
||||
|
||||
Email verification improves security and ensures that users have provided valid email addresses for notifications.
|
||||
@@ -28,6 +28,11 @@
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Access Control",
|
||||
"link": "/docs/rbac",
|
||||
"file": "/rbac.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+221
-45
@@ -3,7 +3,7 @@ title: Triggers | Kener
|
||||
description: Learn how to set up and work with triggers in kener.
|
||||
---
|
||||
|
||||
# Triggers
|
||||
## Triggers
|
||||
|
||||
Triggers are used to trigger actions based on the status of your monitors. You can use triggers to send notifications, or call webhooks when a monitor goes down or up.
|
||||
|
||||
@@ -27,10 +27,36 @@ The description is used to define the description of the webhook. It is optional
|
||||
|
||||
Kener supports the following triggers:
|
||||
|
||||
- [Webhook](#webhook)
|
||||
- [Discord](#discord)
|
||||
- [Slack](#slack)
|
||||
- [Email](#email)
|
||||
- [Webhook](#webhook)
|
||||
- [Discord](#discord)
|
||||
- [Slack](#slack)
|
||||
- [Email](#email)
|
||||
|
||||
### Trigger Variables
|
||||
|
||||
You can use the following variables in the triggers using mustache syntax.
|
||||
|
||||
| Key | Description | Mustache Variable |
|
||||
| ------------- | ------------------------------------------------------------ | ----------------- |
|
||||
| site_name | [string] Name of your site | {{site_name}} |
|
||||
| logo_url | [string] Logo of your site | {{logo_url}} |
|
||||
| site_url | [string] Url of your site | {{site_url}} |
|
||||
| alert_name | [string] Name of the alert | {{alert_name}} |
|
||||
| status | [string] Status of the alert. Can be `TRIGGERED`, `RESOLVED` | {{status}} |
|
||||
| is_resolved | [bool] True if alert is `RESOLVED` | {{is_resolved}} |
|
||||
| is_triggered | [bool] True if alert is `TRIGGERED` | {{is_triggered}} |
|
||||
| description | [string] Description of the alert. | {{description}} |
|
||||
| action_text | [string] CTA for next action on the alert | {{action_text}} |
|
||||
| action_url | [string] URL for next action on the alert | {{action_url}} |
|
||||
| metric | [string] Name of the monitor | {{metric}} |
|
||||
| severity | [string] Severity of the alert. Can be `critical`, `warn` | {{severity}} |
|
||||
| id | [string] Id of the alert | {{id}} |
|
||||
| current_value | [number] Current count of failures | {{current_value}} |
|
||||
| threshold | [number] Threshold Failue | {{threshold}} |
|
||||
| source | [string] Source of the alert | {{source}} |
|
||||
| timestamp | [string] Timestamp **ISO 8601** formatted | {{timestamp}} |
|
||||
|
||||
---
|
||||
|
||||
## Webhook
|
||||
|
||||
@@ -93,24 +119,65 @@ Body of the webhook will be sent as below:
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Description | Variable |
|
||||
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
|
||||
| id | Unique ID of the alert | ${id} |
|
||||
| alert_name | Name of the alert | ${alert_name} |
|
||||
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
|
||||
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
|
||||
| source | Source of the alert. Can be `Kener` | ${source} |
|
||||
| timestamp | Timestamp of the alert | ${timestamp} |
|
||||
| description | Description of the alert. This you can customize. See below | ${description} |
|
||||
| details | Details of the alert. | - |
|
||||
| details.metric | Name of the monitor | ${metric} |
|
||||
| details.current_value | Current value of the monitor | ${current_value} |
|
||||
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
|
||||
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
|
||||
|
||||
### Custom Body
|
||||
|
||||
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
|
||||
Using [mustache variables](#triggers-trigger-variables) you can customize the body of the webhook. If you are not using a json body then please make sure you are using the right content-type by setting custom headers.
|
||||
|
||||
### Examples
|
||||
|
||||
#### 1. Telegram
|
||||
|
||||
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
|
||||
|
||||
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
|
||||
"text": "<b>{{alert_name}}</b>\n\n<b>Severity:</b> <code>{{severity}}</code>\n<b>Status:</b> {{status}}\n<b>Source:</b> Kener\n<b>Time:</b> {{timestamp}}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>{{metric}}\n- <b>Current Value:</b> <code>{{current_value}}</code>\n- <b>Threshold:</b> <code>{{threshold}}</code>\n\n🔍 <a href=\"{{action_url}}\">{{action_text}}</a>",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
|
||||
|
||||
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
|
||||
|
||||
#### 2. Conditional Webhook
|
||||
|
||||
You can use `is_triggered` and `is_resolved` to create conditional webhooks.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for {{metric}} has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}"
|
||||
}
|
||||
```
|
||||
|
||||
The output will be
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for Mockoon has been 🔴 Triggered. Current value is 1 and threshold set is 1"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for Mockoon has been 🟢 Resolved. Current value is 0 and threshold set is 1"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Combing mustache with env variables
|
||||
|
||||
Combine the above example with `NODE_ENV` environment variable.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for {{metric}} in environment $NODE_ENV has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -149,6 +216,60 @@ The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
### Modify Discord Message
|
||||
|
||||
You can modify the discord message by using [mustache variables](#triggers-trigger-variables).
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "{{site_name}}",
|
||||
"avatar_url": "{{{logo_url}}}",
|
||||
"content": "## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [MY CTA]({{{action_url}}}) for more.",
|
||||
"embeds": [
|
||||
{
|
||||
"title": "❌{{alert_name}}❌",
|
||||
"description": "{{description}}",
|
||||
"url": "{{{action_url}}}",
|
||||
"color": "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Monitor",
|
||||
"value": "{{metric}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Severity",
|
||||
"value": "{{severity}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Alert ID",
|
||||
"value": "{{id}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Current Value",
|
||||
"value": "{{current_value}}",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"name": "Threshold",
|
||||
"value": "{{threshold}}",
|
||||
"inline": true
|
||||
}
|
||||
],
|
||||
"footer": {
|
||||
"text": "{{source}}",
|
||||
"icon_url": "{{{logo_url}}}"
|
||||
},
|
||||
"timestamp": "{{timestamp}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slack
|
||||
|
||||
Slack triggers are used to send a message to a slack channel when a monitor goes down or up.
|
||||
@@ -187,6 +308,81 @@ The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
### Modify Slack Message
|
||||
|
||||
You can modify the slack message by using [mustache variables](#triggers-trigger-variables).
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Hello Alert {{alert_name}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Metric:*\n{{metric}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Current Value:*\n{{current_value}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Threshold:*\n{{threshold}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Environment:*\n$_NODE_ENV"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{action_text}}"
|
||||
},
|
||||
"url": "{{{action_url}}}",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
Email triggers are used to send an email when a monitor goes down or up. Kener supports sending emails via [resend](https://resend.com) or over SMTP.
|
||||
@@ -205,10 +401,10 @@ To send emails using Resend you just need to set `RESEND_API_KEY` in the environ
|
||||
|
||||
To send emails using SMTP, please enter
|
||||
|
||||
- Host: SMTP server host
|
||||
- Port: SMTP server port
|
||||
- User: SMTP server username
|
||||
- Password: SMTP server password
|
||||
- Host: SMTP server host
|
||||
- Port: SMTP server port
|
||||
- User: SMTP server username
|
||||
- Password: SMTP server password
|
||||
|
||||
<div class="note danger">
|
||||
|
||||
@@ -273,23 +469,3 @@ Click on the ⚙️ to edit the trigger.
|
||||
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Telegram
|
||||
|
||||
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
|
||||
|
||||
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
|
||||
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
|
||||
|
||||
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
|
||||
|
||||
+16
-17
@@ -1,20 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
Hello world
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
|
||||
></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function up(knex) {
|
||||
return knex.schema.createTable("invitations", (table) => {
|
||||
// Primary key
|
||||
table.increments("id").primary();
|
||||
|
||||
// Core invitation fields
|
||||
table.string("invitation_token").unique().notNullable();
|
||||
table.string("invitation_type").notNullable();
|
||||
table.integer("invited_user_id").nullable();
|
||||
table.integer("invited_by_user_id").notNullable();
|
||||
|
||||
// Additional data fields
|
||||
table.text("invitation_meta").nullable(); // For storing JSON or other metadata
|
||||
table.timestamp("invitation_expiry").notNullable();
|
||||
table.string("invitation_status").notNullable().defaultTo("PENDING");
|
||||
|
||||
// Timestamps
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index("invitation_status");
|
||||
table.index("invitation_expiry");
|
||||
table.index(["invited_by_user_id", "invitation_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function down(knex) {
|
||||
return knex.schema.dropTableIfExists("invitations");
|
||||
}
|
||||
Generated
+267
-46
@@ -1,17 +1,21 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.8",
|
||||
"version": "3.2.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.1.8",
|
||||
"version": "3.2.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
@@ -31,11 +35,12 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mustache": "^4.2.0",
|
||||
"mysql2": "^3.12.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
@@ -46,9 +51,11 @@
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"resend": "^4.0.1",
|
||||
"svelte-codemirror-editor": "^1.4.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
"tailwind-variants": "^0.1.18",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
@@ -60,7 +67,6 @@
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-picker-svelte": "^2.15.1",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -176,10 +182,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||
"dev": true,
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@@ -188,6 +193,113 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
|
||||
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
|
||||
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
|
||||
"integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
|
||||
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
|
||||
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz",
|
||||
"integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.36.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz",
|
||||
"integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@@ -195,7 +307,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -212,7 +323,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -229,7 +339,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -246,7 +355,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -263,7 +371,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -280,7 +387,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -297,7 +403,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -314,7 +419,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -331,7 +435,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -348,7 +451,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -365,7 +467,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -382,7 +483,6 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -399,7 +499,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -416,7 +515,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -433,7 +531,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -450,7 +547,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -467,7 +563,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -484,7 +579,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -501,7 +595,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -518,7 +611,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -535,7 +627,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -552,7 +643,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -756,6 +846,52 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
|
||||
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.21",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
|
||||
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
@@ -776,6 +912,12 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@melt-ui/svelte": {
|
||||
"version": "0.61.2",
|
||||
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.61.2.tgz",
|
||||
@@ -1204,6 +1346,37 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-github": {
|
||||
"version": "4.23.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.23.10.tgz",
|
||||
"integrity": "sha512-jTg2sHAcU1d+8x0O+EBDI71rtJ8PWKIW8gzy+SW4wShQTAdsqGHk5y1ynt3KIeoaUkqngLqAK4SkhPaUKlqZqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-themes": "4.23.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-themes": {
|
||||
"version": "4.23.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.10.tgz",
|
||||
"integrity": "sha512-dU0UgEEgEXCAYpxuVDQ6fovE82XsqgHZckTJOH6Bs8xCi3Z7dwBKO4pXuiA8qGDwTOXOMjSzfi+pRViDm7OfWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@unhead/schema": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.18.tgz",
|
||||
@@ -1525,9 +1698,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -2095,6 +2268,22 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2267,6 +2456,12 @@
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/croner": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/croner/-/croner-7.0.8.tgz",
|
||||
@@ -2994,7 +3189,6 @@
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4779,12 +4973,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "0.292.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.292.0.tgz",
|
||||
"integrity": "sha512-bnTpg9pbm6pQDc+YiLK2yxtRFk2Cc+hbzwjAPaV85k56x10CJ9LsXjon6wRrlNTSdxJR7GOsRjz0A5ZNu3Z7dg==",
|
||||
"version": "0.483.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.483.0.tgz",
|
||||
"integrity": "sha512-MyMgEVLlFfPbyodGpkB+KCpyPkpjI7EKiFw1crA92B1ZXRK5hq5vTsGWAm9Nt3GAKHunoNc5MVsq3EOCz0DZSQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3 <5"
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
@@ -5106,7 +5300,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
@@ -6337,9 +6530,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -6624,7 +6817,6 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
@@ -6771,7 +6963,6 @@
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@@ -7584,6 +7775,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@@ -7794,6 +7991,16 @@
|
||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-codemirror-editor": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-codemirror-editor/-/svelte-codemirror-editor-1.4.1.tgz",
|
||||
"integrity": "sha512-Pv350iro0Y/AZTT/y2OLaonheQqAwl50Hdfipa2Jv1Z04TSP5kPUyxQnRjqxeRW7DXOX9s5Nd11tHdBl9iYSzw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"codemirror": "^6.0.0",
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.57",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.57.tgz",
|
||||
@@ -8419,7 +8626,6 @@
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz",
|
||||
"integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
@@ -8471,6 +8677,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-package-version": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz",
|
||||
"integrity": "sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.0.0-beta.69"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||
@@ -8486,6 +8701,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
+9
-3
@@ -52,7 +52,6 @@
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-picker-svelte": "^2.15.1",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -68,9 +67,13 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
@@ -90,11 +93,12 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mustache": "^4.2.0",
|
||||
"mysql2": "^3.12.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
@@ -105,9 +109,11 @@
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"resend": "^4.0.1",
|
||||
"svelte-codemirror-editor": "^1.4.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
"tailwind-variants": "^0.1.18",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -905,3 +905,37 @@ textarea::placeholder {
|
||||
content: ".";
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.color-picker .container {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.color-picker .color {
|
||||
border: 1px solid hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
.color-picker button {
|
||||
display: none;
|
||||
}
|
||||
.color-picker [aria-label="color picker"] input {
|
||||
color: hsl(var(--foreground) / var(--tw-border-opacity)) !important;
|
||||
background-color: hsl(var(--input) / var(--tw-text-opacity, 1)) !important;
|
||||
}
|
||||
|
||||
.dark .color-picker {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
--cp-input-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
--cp-button-hover-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
.like-input {
|
||||
background-color: rgba(0, 0, 0, 0.02) !important;
|
||||
}
|
||||
.dark .like-input {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
import { onMount, onDestroy, beforeUpdate } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let monitor;
|
||||
export let theme = "light";
|
||||
export let bgc = "transparent";
|
||||
export let locale = "en";
|
||||
|
||||
let iframe, listeners;
|
||||
let containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
let isMounted = false;
|
||||
let prevMonitor, prevTheme, prevBgc, prevLocale;
|
||||
|
||||
onMount(() => {
|
||||
if (!monitor) return;
|
||||
|
||||
isMounted = true;
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const uid = "KENER_" + ~~(new Date().getTime() / 86400000);
|
||||
const uriEmbedded = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.src = uriEmbedded;
|
||||
iframe.id = uid;
|
||||
iframe.width = "0%";
|
||||
iframe.height = "0";
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allowTransparency = true;
|
||||
iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(iframe);
|
||||
|
||||
const setHeight = (data) => {
|
||||
if (data.height !== undefined) {
|
||||
iframe.height = data.height;
|
||||
}
|
||||
};
|
||||
|
||||
const setWidth = (data) => {
|
||||
if (data.width !== undefined) {
|
||||
iframe.width = data.width;
|
||||
}
|
||||
};
|
||||
|
||||
listeners = (event) => {
|
||||
if (event.data && event.data.height !== undefined) {
|
||||
setHeight(event.data);
|
||||
}
|
||||
if (event.data && event.data.width !== undefined) {
|
||||
setWidth(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", listeners, false);
|
||||
});
|
||||
|
||||
beforeUpdate(() => {
|
||||
if (
|
||||
isMounted &&
|
||||
iframe &&
|
||||
(monitor !== prevMonitor || theme !== prevTheme || bgc !== prevBgc || locale !== prevLocale)
|
||||
) {
|
||||
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
// Update previous values
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
dispatch("update", {
|
||||
monitor: prevMonitor,
|
||||
theme: prevTheme,
|
||||
bgc: prevBgc,
|
||||
locale: prevLocale
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (iframe) {
|
||||
window.removeEventListener("message", listeners);
|
||||
iframe.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dynamic container with unique ID -->
|
||||
<div id={containerId}></div>
|
||||
@@ -0,0 +1,194 @@
|
||||
export const DiscordJSONTemplate = JSON.stringify(
|
||||
{
|
||||
username: "{{site_name}}",
|
||||
avatar_url: "{{{logo_url}}}",
|
||||
content:
|
||||
"## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [{{action_text}}]({{{action_url}}}) for more.",
|
||||
embeds: [
|
||||
{
|
||||
title: "{{alert_name}}",
|
||||
description: "{{description}}",
|
||||
url: "{{{action_url}}}",
|
||||
color: "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
|
||||
fields: [
|
||||
{
|
||||
name: "Monitor",
|
||||
value: "{{metric}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Severity",
|
||||
value: "{{severity}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Alert ID",
|
||||
value: "{{id}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Current Value",
|
||||
value: "{{current_value}}",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Threshold",
|
||||
value: "{{threshold}}",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: "{{source}}",
|
||||
icon_url: "{{{logo_url}}}",
|
||||
},
|
||||
timestamp: "{{timestamp}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const WebhookJSONTemplate = JSON.stringify(
|
||||
{
|
||||
id: "{{id}}",
|
||||
alert_name: "{{alert_name}}",
|
||||
severity: "{{severity}}",
|
||||
status: "{{status}}",
|
||||
source: "{{source}}",
|
||||
timestamp: "{{timestamp}}",
|
||||
description: "{{description}}",
|
||||
details: {
|
||||
metric: "{{metric}}",
|
||||
current_value: "{{current_value}}",
|
||||
threshold: "{{threshold}}",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
text: "{{action_text}}",
|
||||
url: "{{action_url}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const SlackJSONTemplate = JSON.stringify(
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{alert_name}}",
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Metric:*\n{{metric}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Current Value:*\n{{current_value}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Threshold:*\n{{threshold}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{action_text}}",
|
||||
},
|
||||
url: "{{{action_url}}}",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const DefaultAPIEval = `(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const DefaultPingEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const DefaultTCPEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.status === "open";
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="30" cy="24" r="10"/>
|
||||
<path d="M26 27h8"/>
|
||||
<path d="M26 21h2"/>
|
||||
<path d="M32 21h2"/>
|
||||
<text x="80" y="29" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" fill="currentColor" font-weight="300">Not Found</text>
|
||||
</svg>`;
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
import { Settings, ArrowRight } from "lucide-svelte";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
import { l, f, fd, fdn } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
@@ -78,7 +79,7 @@
|
||||
<div class="col-span-12">
|
||||
<Accordion.Root bind:value={index} class="accor">
|
||||
<Accordion.Item value={accordionValue}>
|
||||
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
|
||||
<Accordion.Trigger class="rounded-md px-4 hover:bg-muted hover:no-underline">
|
||||
<div class="w-full text-left hover:no-underline">
|
||||
<p class="flex gap-x-2 text-xs font-semibold">
|
||||
{#if incidentType == "INCIDENT"}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
export let data;
|
||||
let pageNo = 1;
|
||||
let limit = 20;
|
||||
|
||||
@@ -1,271 +1,251 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, Copy, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Bell from "lucide-svelte/icons/bell";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Copy from "lucide-svelte/icons/copy";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if !!newKeyResp && !!newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source
|
||||
srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif"
|
||||
alt="🎉"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p
|
||||
class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium"
|
||||
>
|
||||
{newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp" type="image/webp" />
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif" alt="🎉" width="24" height="24" />
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium">
|
||||
{newKeyResp.apiKey}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="copybtn absolute right-2 top-2 h-5 w-5 p-1"
|
||||
on:click={copyKey}
|
||||
>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline"
|
||||
>not be shown again</b
|
||||
>, so make sure to save it.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" class="copybtn absolute right-2 top-2 h-5 w-5 p-1" on:click={copyKey}>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline">not be shown again</b>, so make sure
|
||||
to save it.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Key</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{moment
|
||||
.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss")
|
||||
.local()
|
||||
.format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Key</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
|
||||
{moment.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={$page.data.user.role == "member"}
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 peer-disabled:opacity-55 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if showCreateModal}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to
|
||||
your account and should be kept secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to your account and should be kept
|
||||
secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
required
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
import locales from "$lib/locales/locales.json?raw";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
|
||||
import { Loader, X, Plus, Info, Play } from "lucide-svelte";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Play from "lucide-svelte/icons/play";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
export let data;
|
||||
@@ -87,8 +91,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let heroError = "";
|
||||
async function formSubmitHero() {
|
||||
heroError = "";
|
||||
formStateHero = "loading";
|
||||
let resp = await storeSiteData({
|
||||
hero: JSON.stringify(hero)
|
||||
@@ -97,7 +102,7 @@
|
||||
let data = await resp.json();
|
||||
formStateHero = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
heroError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +122,10 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let categoriesErrorMessage = "";
|
||||
async function formSubmitCategories() {
|
||||
categoriesErrorMessage = "";
|
||||
formStateCategories = "loading";
|
||||
let resp = await storeSiteData({
|
||||
categories: JSON.stringify(categories)
|
||||
@@ -126,11 +134,14 @@
|
||||
let data = await resp.json();
|
||||
formStateCategories = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
categoriesErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let footerErrorMessage = "";
|
||||
async function formSubmitFooter() {
|
||||
footerErrorMessage = "";
|
||||
formStateFooter = "loading";
|
||||
let resp = await storeSiteData({
|
||||
footerHTML: footerHTML
|
||||
@@ -139,11 +150,14 @@
|
||||
let data = await resp.json();
|
||||
formStateFooter = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
footerErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let incidentErrorMessage = "";
|
||||
async function formSubmitIncident() {
|
||||
incidentErrorMessage = "";
|
||||
formStateIncident = "loading";
|
||||
let resp = await storeSiteData({
|
||||
homeIncidentCount: homeIncidentCount,
|
||||
@@ -154,7 +168,7 @@
|
||||
let data = await resp.json();
|
||||
formStateIncident = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
incidentErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +234,9 @@
|
||||
i18n.defaultLocale = e.value;
|
||||
}
|
||||
|
||||
let i18nErrorMessage = "";
|
||||
async function formSubmiti18n() {
|
||||
i18nErrorMessage = "";
|
||||
formStatei18n = "loading";
|
||||
let resp = await storeSiteData({
|
||||
i18n: JSON.stringify(i18n),
|
||||
@@ -230,7 +246,7 @@
|
||||
let data = await resp.json();
|
||||
formStatei18n = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
i18nErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -289,7 +305,10 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!heroError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
{#if formStateHero === "loading"}
|
||||
@@ -362,6 +381,9 @@
|
||||
Translates to "Show {homeIncidentCount} incidents that have started in the last {homeIncidentStartTimeWithin}
|
||||
days in the past or going to start in {homeIncidentStartTimeWithin} days in the future"
|
||||
</p>
|
||||
{#if !!incidentErrorMessage}
|
||||
<p class="text-sm font-medium text-destructive">{incidentErrorMessage}</p>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -470,7 +492,7 @@
|
||||
</div>
|
||||
<div class="flex w-full justify-end" style="margin-top:32px">
|
||||
<p class="px-4 pt-1">
|
||||
<span class="text-xs text-red-500"> {navErrorMessage} </span>
|
||||
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
|
||||
</p>
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
@@ -547,7 +569,10 @@
|
||||
Allow users to switch timezones
|
||||
</label>
|
||||
</p>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!i18nErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStatei18n === "loading"}>
|
||||
Save
|
||||
{#if formStatei18n === "loading"}
|
||||
@@ -607,7 +632,10 @@
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!categoriesErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateCategories === "loading"}>
|
||||
Save
|
||||
{#if formStateCategories === "loading"}
|
||||
@@ -636,7 +664,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!footerErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save
|
||||
{#if formStateFooter === "loading"}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Clipboard, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import { ChevronRight, Trash } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Clipboard from "lucide-svelte/icons/clipboard";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import Trash from "lucide-svelte/icons/trash";
|
||||
import { base } from "$app/paths";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
import {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
@@ -100,8 +109,8 @@
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// let evalResp = await eval(newMonitor.apiConfig.eval + `(200, 1000, "e30=")`);
|
||||
new Function(ev);
|
||||
|
||||
return true; // The code is valid
|
||||
} catch (error) {
|
||||
invalidFormMessage = error.message + " in eval.";
|
||||
@@ -718,6 +727,18 @@
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-6">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
newMonitor.apiConfig.allowSelfSignedCert = e.target.checked;
|
||||
}}
|
||||
checked={newMonitor.apiConfig.allowSelfSignedCert}
|
||||
/>
|
||||
<span class="ml-2 text-sm">Allow Self Signed Certificate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<Label for="eval">Eval</Label>
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
@@ -728,12 +749,23 @@
|
||||
href="https://kener.ing/docs/monitors-api#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.apiConfig.eval}
|
||||
id="eval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.apiConfig.eval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "18rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "PING"}
|
||||
@@ -823,12 +855,23 @@
|
||||
href="https://kener.ing/docs/monitors-ping#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.pingConfig.pingEval}
|
||||
id="pingEval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.pingConfig.pingEval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "18rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,12 +957,22 @@
|
||||
href="https://kener.ing/docs/monitors-tcp#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.tcpConfig.tcpEval}
|
||||
id="tcpEval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.tcpConfig.tcpEval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "22rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip, ExternalLink } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Bell from "lucide-svelte/icons/bell";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ArrowDownUp from "lucide-svelte/icons/arrow-down-up";
|
||||
import Grip from "lucide-svelte/icons/grip";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -13,6 +20,8 @@
|
||||
import { dndzone } from "svelte-dnd-action";
|
||||
import { flip } from "svelte/animate";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { DefaultAPIEval, DefaultTCPEval, DefaultPingEval } from "$lib/anywhere.js";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
@@ -70,16 +79,17 @@
|
||||
headers: [],
|
||||
body: "",
|
||||
timeout: 10000,
|
||||
eval: "",
|
||||
hideURLForGet: "NO"
|
||||
eval: DefaultAPIEval,
|
||||
hideURLForGet: "NO",
|
||||
allowSelfSignedCert: false
|
||||
},
|
||||
tcpConfig: {
|
||||
hosts: [], //{timeout: 1000, host: "", type:""}
|
||||
tcpEval: ""
|
||||
tcpEval: DefaultTCPEval
|
||||
},
|
||||
pingConfig: {
|
||||
hosts: [], //{timeout: 1000, host: "", count: "", type:""}
|
||||
pingEval: ""
|
||||
pingEval: DefaultPingEval
|
||||
},
|
||||
dnsConfig: {
|
||||
host: "",
|
||||
@@ -242,7 +252,9 @@
|
||||
}
|
||||
};
|
||||
|
||||
let saveTriggerError = "";
|
||||
async function saveTriggers() {
|
||||
saveTriggerError = "";
|
||||
let data = {
|
||||
id: currentAlertMonitor.id,
|
||||
down_trigger: JSON.stringify(monitorTriggers.down_trigger),
|
||||
@@ -258,12 +270,18 @@
|
||||
},
|
||||
body: JSON.stringify({ action: "updateMonitorTriggers", data })
|
||||
});
|
||||
|
||||
let resp = await apiResp.json();
|
||||
if (resp.error) {
|
||||
saveTriggerError = resp.error;
|
||||
} else {
|
||||
shareMenusToggle = false;
|
||||
loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
saveTriggerError = "Error while saving triggers";
|
||||
} finally {
|
||||
formState = "idle";
|
||||
loadData();
|
||||
shareMenusToggle = false;
|
||||
}
|
||||
}
|
||||
let currentAlertMonitor;
|
||||
@@ -278,15 +296,27 @@
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
|
||||
let orderErrorMessage = "";
|
||||
function handleSort(e) {
|
||||
dropTargetStyle = {
|
||||
border: "1px solid transparent"
|
||||
};
|
||||
monitors = e.detail.items;
|
||||
monitorSort = monitors.map((m) => m.id);
|
||||
|
||||
storeSiteData({
|
||||
monitorSort: JSON.stringify(monitorSort)
|
||||
});
|
||||
})
|
||||
.then(async (resp) => {
|
||||
let data = await resp.json();
|
||||
if (data.error) {
|
||||
orderErrorMessage = data.error;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
orderErrorMessage = "Error while saving order";
|
||||
});
|
||||
}
|
||||
let dropTargetStyle;
|
||||
let draggableMenu = false;
|
||||
@@ -360,6 +390,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !!orderErrorMessage}
|
||||
<p class="py-2 text-sm font-medium text-destructive">{orderErrorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,17 +456,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button size="icon" variant="secondary" on:click={() => (draggableMenu = !draggableMenu)}>
|
||||
<ArrowDownUp class=" " />
|
||||
{#if $page.data.user.role != "member"}
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button size="icon" variant="secondary" on:click={() => (draggableMenu = !draggableMenu)}>
|
||||
<ArrowDownUp class=" " />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -452,7 +487,7 @@
|
||||
<div class="absolute right-2 top-0.5 flex gap-x-1">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="secondary" class="h-8 p-2 text-xs" on:click={() => testMonitor(i)}>TEST</Button>
|
||||
<Button variant="secondary" class="h-8 p-2 text-xs" on:click={() => testMonitor(i)}>Test Monitor</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-w-md">
|
||||
<DropdownMenu.Group>
|
||||
@@ -482,22 +517,25 @@
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-8 w-8 p-2 {monitor.down_trigger?.active || monitor.degraded_trigger?.active
|
||||
? 'text-yellow-500'
|
||||
: ''}"
|
||||
on:click={() => openAlertMenu(monitor)}
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-8 w-8 p-2 {monitor.down_trigger?.active || monitor.degraded_trigger?.active
|
||||
? 'text-yellow-500'
|
||||
: ''}"
|
||||
on:click={() => openAlertMenu(monitor)}
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
|
||||
<ExternalLink class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
@@ -677,9 +715,12 @@
|
||||
<hr class="my-4" />
|
||||
{/each}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button class="w-full" on:click={saveTriggers} disabled={formState === "loading"}>
|
||||
Save
|
||||
<div class="flex justify-end gap-x-2">
|
||||
{#if !!saveTriggerError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{saveTriggerError}</div>
|
||||
{/if}
|
||||
<Button class="" on:click={saveTriggers} disabled={formState === "loading"}>
|
||||
Save Alerts
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -1,221 +1,213 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Plus, X } from "lucide-svelte";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader, Info } from "lucide-svelte";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
|
||||
let metaTags = [];
|
||||
function addNewRow() {
|
||||
metaTags = [
|
||||
...metaTags,
|
||||
{
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
];
|
||||
}
|
||||
let analyticsSupported = [
|
||||
{
|
||||
id: "",
|
||||
type: "GA",
|
||||
name: "Google Analytics",
|
||||
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "AMPLITUDE",
|
||||
name: "Amplitude",
|
||||
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "MIXPANEL",
|
||||
name: "MixPanel",
|
||||
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
|
||||
}
|
||||
];
|
||||
function removeRow(i) {
|
||||
metaTags = metaTags.filter((_, index) => index !== i);
|
||||
}
|
||||
let metaTags = [];
|
||||
function addNewRow() {
|
||||
metaTags = [
|
||||
...metaTags,
|
||||
{
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
];
|
||||
}
|
||||
let analyticsSupported = [
|
||||
{
|
||||
id: "",
|
||||
type: "GA",
|
||||
name: "Google Analytics",
|
||||
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "AMPLITUDE",
|
||||
name: "Amplitude",
|
||||
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "MIXPANEL",
|
||||
name: "MixPanel",
|
||||
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
|
||||
}
|
||||
];
|
||||
function removeRow(i) {
|
||||
metaTags = metaTags.filter((_, index) => index !== i);
|
||||
}
|
||||
|
||||
metaTags = siteDataExtractFromDb(data.siteData, {
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
}).metaTags;
|
||||
metaTags = siteDataExtractFromDb(data.siteData, {
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
}).metaTags;
|
||||
|
||||
let analytics = siteDataExtractFromDb(data.siteData, {
|
||||
analytics: []
|
||||
}).analytics;
|
||||
let analytics = siteDataExtractFromDb(data.siteData, {
|
||||
analytics: []
|
||||
}).analytics;
|
||||
|
||||
//merge analyticsSupported with analytics
|
||||
analytics = analyticsSupported.map((supported) => {
|
||||
let found = analytics.find((a) => a.type === supported.type);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
return supported;
|
||||
});
|
||||
//merge analyticsSupported with analytics
|
||||
analytics = analyticsSupported.map((supported) => {
|
||||
let found = analytics.find((a) => a.type === supported.type);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
return supported;
|
||||
});
|
||||
|
||||
let formState = "idle";
|
||||
let formStateAnalytics = "idle";
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData({
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let formState = "idle";
|
||||
let formStateAnalytics = "idle";
|
||||
|
||||
async function formSubmitAnalytics() {
|
||||
formStateAnalytics = "loading";
|
||||
let resp = await storeSiteData({
|
||||
analytics: JSON.stringify(analytics)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateAnalytics = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let metaError = "";
|
||||
async function formSubmit() {
|
||||
metaError = "";
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData({
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
metaError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let seoError = "";
|
||||
async function formSubmitAnalytics() {
|
||||
seoError = "";
|
||||
formStateAnalytics = "loading";
|
||||
let resp = await storeSiteData({
|
||||
analytics: JSON.stringify(analytics)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateAnalytics = "idle";
|
||||
if (data.error) {
|
||||
seoError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Analytics</Card.Title>
|
||||
<Card.Description>
|
||||
Add your analytics ID/Key here. You can add multiple analytics providers.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each analytics as analytic}
|
||||
<div class="col-span-1">
|
||||
<Label for={analytic.type}>
|
||||
{analytic.name}
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Add your {analytic.name} ID/Key here.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={analytic.id}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id={analytic.type}
|
||||
placeholder="{analytic.type} ID/Key"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Analytics</Card.Title>
|
||||
<Card.Description>Add your analytics ID/Key here. You can add multiple analytics providers.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each analytics as analytic}
|
||||
<div class="col-span-1">
|
||||
<Label for={analytic.type}>
|
||||
{analytic.name}
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Add your {analytic.name} ID/Key here.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={analytic.id}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id={analytic.type}
|
||||
placeholder="{analytic.type} ID/Key"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<Button type="submit" disabled={formStateAnalytics === "loading"}>
|
||||
Save
|
||||
{#if formStateAnalytics === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!seoError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateAnalytics === "loading"}>
|
||||
Save
|
||||
{#if formStateAnalytics === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Search Engine Optimization</Card.Title>
|
||||
<Card.Description>
|
||||
Add your meta tags here. You can add multiple meta tags.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
|
||||
{#each metaTags as metaTag, i}
|
||||
<div class="flex w-full flex-row justify-between gap-2">
|
||||
<div class="grid grid-cols-12 gap-x-2">
|
||||
<div class="col-span-4">
|
||||
<Label for="key">
|
||||
Meta Name
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
<meta name="{metaTag.key}" content="{metaTag.value}">
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={metaTag.key}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="key"
|
||||
placeholder="eg. kener"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-7">
|
||||
<Label for="value">Content</Label>
|
||||
<Input
|
||||
bind:value={metaTag.value}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="value"
|
||||
placeholder="eg. kener"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="relative">
|
||||
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Search Engine Optimization</Card.Title>
|
||||
<Card.Description>Add your meta tags here. You can add multiple meta tags.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
|
||||
{#each metaTags as metaTag, i}
|
||||
<div class="flex w-full flex-row justify-between gap-2">
|
||||
<div class="grid grid-cols-12 gap-x-2">
|
||||
<div class="col-span-4">
|
||||
<Label for="key">
|
||||
Meta Name
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
<meta name="{metaTag.key}" content="{metaTag.value}">
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input bind:value={metaTag.key} class="mt-2" type="text" id="key" placeholder="eg. kener" />
|
||||
</div>
|
||||
<div class="col-span-7">
|
||||
<Label for="value">Content</Label>
|
||||
<Input bind:value={metaTag.value} class="mt-2" type="text" id="value" placeholder="eg. kener" />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="relative">
|
||||
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
|
||||
|
||||
<Button
|
||||
on:click={addNewRow}
|
||||
variant="secondary"
|
||||
class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 "
|
||||
>
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end" style="margin-top:32px">
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<Button on:click={addNewRow} variant="secondary" class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 ">
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end gap-x-2" style="margin-top:32px">
|
||||
{#if !!metaError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { tooltipAction } from "svelte-legos";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader, Info, X } from "lucide-svelte";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
@@ -431,11 +433,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<p class="px-4 pt-3 text-sm">
|
||||
<span class="text-red-500">
|
||||
{formErrorMessage}
|
||||
</span>
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
<p class="py-2 text-sm font-medium text-destructive">
|
||||
{formErrorMessage}
|
||||
</p>
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Plus, X, Info } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader } from "lucide-svelte";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import ColorPicker from "svelte-awesome-color-picker";
|
||||
@@ -15,6 +17,7 @@
|
||||
export let data;
|
||||
|
||||
let formState = "idle";
|
||||
let themeError = "";
|
||||
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
@@ -49,6 +52,7 @@
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
themeError = "";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
@@ -59,7 +63,7 @@
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
themeError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -345,7 +349,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!themeError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Settings, Check, ChevronRight } from "lucide-svelte";
|
||||
import { page } from "$app/stores";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -9,8 +15,14 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { IsValidURL } from "$lib/clientTools.js";
|
||||
import { DiscordJSONTemplate, WebhookJSONTemplate, SlackJSONTemplate } from "$lib/anywhere.js";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let status = "ACTIVE";
|
||||
let formState = "idle";
|
||||
let showAddTrigger = false;
|
||||
@@ -29,8 +41,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -52,8 +68,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -91,13 +111,35 @@
|
||||
|
||||
async function addNewTrigger() {
|
||||
invalidFormMessage = "";
|
||||
formState = "loading";
|
||||
// formState = "loading";
|
||||
//newTrigger.trigger_type present not empty
|
||||
if (newTrigger.trigger_type == "") {
|
||||
invalidFormMessage = "Trigger Type is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
if (newTrigger.trigger_type == "discord") {
|
||||
if (newTrigger.trigger_meta.has_discord_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.discord_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Discord Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newTrigger.trigger_type == "slack") {
|
||||
if (newTrigger.trigger_meta.has_slack_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.slack_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Slack Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTrigger.trigger_type == "email") {
|
||||
newTrigger.trigger_meta.url = "";
|
||||
@@ -346,10 +388,12 @@
|
||||
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -382,12 +426,13 @@
|
||||
{#if trigger.testLoaders == "success"}
|
||||
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
|
||||
{/if}
|
||||
<span class="h-4 text-xs font-medium"> TEST </span>
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
<span class="h-4 text-xs font-medium"> Test Trigger </span>
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
@@ -551,7 +596,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_webhook_body = e.target.checked;
|
||||
@@ -567,21 +612,116 @@
|
||||
set. The default is JSON. There are <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#webhook-body">variables</a
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">mustache variables</a
|
||||
>
|
||||
that you can use for the webhook body. To use a variable wrap it like
|
||||
<code>${variable}</code>
|
||||
<code>{{variable}}</code>. There are some examples
|
||||
<a target="_blank" class="text-blue-500" href="https://kener.ing/docs/triggers#webhook-examples"
|
||||
>here</a
|
||||
>
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.webhook_body}
|
||||
class="mt-2 h-[500px] w-full rounded-sm border p-2"
|
||||
placeholder={placeholderWebhookBody}
|
||||
></textarea>
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "slack"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_slack_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_slack_body}
|
||||
/>
|
||||
Use Custom Slack Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_slack_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom slack body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the slack body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.slack_body}
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "discord"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_discord_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_discord_body}
|
||||
/>
|
||||
Use Custom Discord Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_discord_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom discord body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the discord body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.discord_body}
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "email"}
|
||||
<div class="mt-4 w-full">
|
||||
<RadioGroup.Root class="my-4 flex" bind:value={newTrigger.trigger_meta.email_type}>
|
||||
@@ -675,11 +815,11 @@
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
<div class="col-span-5 py-2.5">
|
||||
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
|
||||
<p class="text-right text-sm font-medium text-destructive">{invalidFormMessage}</p>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button class="col-span-1 w-full" disabled={formState === "loading"} on:click={addNewTrigger}>
|
||||
Save
|
||||
Save Trigger
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import { Share2, ArrowRight, Settings, TrendingUp, Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { afterUpdate } from "svelte";
|
||||
@@ -229,16 +235,6 @@
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
{#if monitor.monitor_type === "GROUP"}
|
||||
<Button
|
||||
class="bounce-right h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
>
|
||||
<ArrowRight class="arrow h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +315,6 @@
|
||||
></div>
|
||||
{#if !!incidents[ts]}
|
||||
<div
|
||||
style="transition-delay: {Math.floor(Math.random() * (1500 - 500 + 1)) + 500}ms;"
|
||||
class="bg-api-{incidents[
|
||||
ts
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
|
||||
@@ -337,6 +332,21 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if monitor.monitor_type === "GROUP" && !!!embed}
|
||||
<div class="-mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
class="bounce-right h-8 text-xs"
|
||||
on:click={scrollToRight}
|
||||
>
|
||||
{l(lang, "View in detail")}
|
||||
<ArrowRight class="arrow ml-1.5 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showDailyDataModal}
|
||||
<div
|
||||
transition:slide={{ direction: "bottom" }}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages, Menu } from "lucide-svelte";
|
||||
import Languages from "lucide-svelte/icons/languages";
|
||||
import Menu from "lucide-svelte/icons/menu";
|
||||
import { base } from "$app/paths";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script>
|
||||
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader, ExternalLink } from "lucide-svelte";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import Link from "lucide-svelte/icons/link";
|
||||
import CopyCheck from "lucide-svelte/icons/copy-check";
|
||||
import Code from "lucide-svelte/icons/code";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Percent from "lucide-svelte/icons/percent";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AccordionPrimitive.TriggerProps;
|
||||
type $$Events = AccordionPrimitive.TriggerEvents;
|
||||
type $$Props = AccordionPrimitive.TriggerProps;
|
||||
type $$Events = AccordionPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Header {level} class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Circle } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { ChevronRight } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import { Circle } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = RadioGroupPrimitive.ItemProps;
|
||||
type $$Events = RadioGroupPrimitive.ItemEvents;
|
||||
type $$Props = RadioGroupPrimitive.ItemProps;
|
||||
type $$Events = RadioGroupPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
{value}
|
||||
class={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
{value}
|
||||
class={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<RadioGroupPrimitive.ItemIndicator>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.ItemIndicator>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<RadioGroupPrimitive.ItemIndicator>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.ItemIndicator>
|
||||
</div>
|
||||
</RadioGroupPrimitive.Item>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</SelectPrimitive.Item>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</SelectPrimitive.Trigger>
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "VERFÜGBAR",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit"
|
||||
"Uptime": "Verfügbarkeit",
|
||||
"View in detail": "Details anzeigen"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis i detaljer"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "View in detail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "En ligne",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement"
|
||||
"Uptime": "Temps de fonctionnement",
|
||||
"View in detail": "Voir en détail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "अप",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम"
|
||||
"Uptime": "अपटाइम",
|
||||
"View in detail": "विस्तार में देखें"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "稼働中",
|
||||
"Upcoming Maintenance": "今後のメンテナンス",
|
||||
"Updates": "更新",
|
||||
"Uptime": "稼働時間"
|
||||
"Uptime": "稼働時間",
|
||||
"View in detail": "詳細を表示"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "작동 중",
|
||||
"Upcoming Maintenance": "예정된 유지보수",
|
||||
"Updates": "업데이트",
|
||||
"Uptime": "업타임"
|
||||
"Uptime": "업타임",
|
||||
"View in detail": "자세히 보기"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis detaljer"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "BEREIKBAAR",
|
||||
"Upcoming Maintenance": "Aankomend onderhoud",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "Bekijk in detail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPERACIONAL",
|
||||
"Upcoming Maintenance": "Manutenção Agendada",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade"
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"View in detail": "Ver em detalhes"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Uptime": "Время работы"
|
||||
"Uptime": "Время работы",
|
||||
"View in detail": "Подробный просмотр"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming Maintenance": "Yaklaşan Bakım",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi"
|
||||
"Uptime": "Çalışma Süresi",
|
||||
"View in detail": "Detaylı Görüntüle"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "Hoạt động",
|
||||
"Upcoming Maintenance": "Bảo trì sắp tới",
|
||||
"Updates": "Cập nhật",
|
||||
"Uptime": "Thời gian hoạt động"
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"View in detail": "Xem chi tiết"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "可用",
|
||||
"Upcoming Maintenance": "即将进行的维护",
|
||||
"Updates": "更新",
|
||||
"Uptime": "正常运行时间"
|
||||
"Uptime": "正常运行时间",
|
||||
"View in detail": "详细查看"
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
const DEFAULT_STATUS = "default_status";
|
||||
const SIGNAL = "signal";
|
||||
const INVITE_VERIFY_EMAIL = "invite_verify_email";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -123,4 +124,5 @@ export {
|
||||
WEBHOOK,
|
||||
DEFAULT_STATUS,
|
||||
SIGNAL,
|
||||
INVITE_VERIFY_EMAIL,
|
||||
};
|
||||
|
||||
@@ -15,10 +15,13 @@ import {
|
||||
import db from "../db/db.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Resend } from "resend";
|
||||
|
||||
import crypto from "crypto";
|
||||
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
|
||||
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "../tool.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences } from "../tool.js";
|
||||
import getSMTPTransport from "../notification/smtps.js";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -302,6 +305,23 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
|
||||
export const GetLatestMonitoringData = async (monitor_tag) => {
|
||||
return await db.getLatestMonitoringData(monitor_tag);
|
||||
};
|
||||
export const GetLatestStatusActiveAll = async (monitor_tags) => {
|
||||
let latestData = await db.getLatestMonitoringDataAllActive(monitor_tags);
|
||||
let status = "NO_DATA";
|
||||
for (let i = 0; i < latestData.length; i++) {
|
||||
//if any status is down then status = down, if any is degraded then status = degraded, down > degraded > up
|
||||
if (latestData[i].status === "DOWN") {
|
||||
status = "DOWN";
|
||||
} else if (latestData[i].status === "DEGRADED" && status !== "DOWN") {
|
||||
status = "DEGRADED";
|
||||
} else if (latestData[i].status === "UP" && status !== "DOWN" && status !== "DEGRADED") {
|
||||
status = "UP";
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: status,
|
||||
};
|
||||
};
|
||||
export const GetLastHeartbeat = async (monitor_tag) => {
|
||||
return await db.getLastHeartbeat(monitor_tag);
|
||||
};
|
||||
@@ -397,6 +417,12 @@ export const CreateNewAPIKey = async (data) => {
|
||||
const apiKey = generateApiKey();
|
||||
const hashed_key = await createHash(apiKey);
|
||||
//insert into db
|
||||
|
||||
//data.name cant be empty
|
||||
if (!data.name) {
|
||||
throw new Error("Name is required");
|
||||
}
|
||||
|
||||
await db.createNewApiKey({
|
||||
name: data.name,
|
||||
hashed_key: hashed_key,
|
||||
@@ -438,12 +464,6 @@ export const IsSetupComplete = async () => {
|
||||
return data.length > 0;
|
||||
};
|
||||
|
||||
export const HashString = (str) => {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(str);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const InterpolateData = (rawData, startTimestamp, initialStatus, overrideEndTimestamp) => {
|
||||
const interpolatedData = [];
|
||||
let currentStatus = initialStatus || "UP";
|
||||
@@ -479,6 +499,19 @@ export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
}
|
||||
return NO_DATA;
|
||||
};
|
||||
export const GetMonitoringData = async (tag, since, now) => {
|
||||
return await db.getMonitoringData(tag, since, now);
|
||||
};
|
||||
export const GetMonitoringDataAll = async (tags, since, now) => {
|
||||
return await db.getMonitoringDataAll(tags, since, now);
|
||||
};
|
||||
export const GetLastStatusBeforeAll = async (monitor_tags, timestamp) => {
|
||||
let data = await db.getLastStatusBeforeAll(monitor_tags, timestamp);
|
||||
if (data) {
|
||||
return data.status;
|
||||
}
|
||||
return NO_DATA;
|
||||
};
|
||||
|
||||
export const AggregateData = (rawData) => {
|
||||
//data like [{ timestamp: 1732435920, status: 'NO_DATA' }]
|
||||
@@ -899,6 +932,250 @@ export const GetSMTPFromENV = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const IsResendSetup = () => {
|
||||
return !!process.env.RESEND_API_KEY && !!process.env.RESEND_SENDER_EMAIL;
|
||||
};
|
||||
|
||||
export const IsEmailSetup = () => {
|
||||
return !!GetSMTPFromENV || IsResendSetup();
|
||||
};
|
||||
|
||||
export const UpdateUserData = async (data) => {
|
||||
let userID = data.userID;
|
||||
let updateKey = data.updateKey;
|
||||
let updateValue = data.updateValue;
|
||||
|
||||
//if updateKey is password, throw error
|
||||
if (updateKey === "password") {
|
||||
throw new Error("Password cannot be updated using this method");
|
||||
}
|
||||
//if updateValue is empty, throw error
|
||||
if (!!!updateValue) {
|
||||
throw new Error("Update value cannot be empty");
|
||||
}
|
||||
|
||||
switch (updateKey) {
|
||||
case "name":
|
||||
return await db.updateUserName(userID, updateValue);
|
||||
case "role":
|
||||
return await db.updateUserRole(userID, updateValue);
|
||||
case "is_verified":
|
||||
return await db.updateIsVerified(userID, updateValue);
|
||||
default:
|
||||
throw new Error("Invalid update key");
|
||||
}
|
||||
};
|
||||
|
||||
export const CreateNewInvitation = async (data) => {
|
||||
let invite = {};
|
||||
|
||||
//create a token
|
||||
let token = crypto.randomBytes(32).toString("hex");
|
||||
let hashedToken = createHash(token);
|
||||
let invitation_token = data.invitation_type.toLowerCase() + "_" + hashedToken;
|
||||
|
||||
invite.invitation_token = invitation_token;
|
||||
invite.invitation_type = data.invitation_type;
|
||||
invite.invited_user_id = data.invited_user_id;
|
||||
invite.invited_by_user_id = data.invited_by_user_id;
|
||||
invite.invitation_meta = data.invitation_meta;
|
||||
invite.invitation_expiry = data.invitation_expiry;
|
||||
invite.invitation_status = "PENDING";
|
||||
|
||||
//update old invitations to VOID
|
||||
await db.updateInvitationStatusToVoid(invite.invited_user_id, invite.invitation_type);
|
||||
|
||||
let res = await db.insertInvitation(invite);
|
||||
return {
|
||||
invitation_token,
|
||||
};
|
||||
};
|
||||
|
||||
//check if there is a row for given invited_user_id,invitation_type and invitation_status = PENDING
|
||||
export const CheckInvitationExists = async (invited_user_id, invitation_type) => {
|
||||
let invitation = await db.invitationExists(invited_user_id, invitation_type);
|
||||
return !!invitation;
|
||||
};
|
||||
|
||||
//getInvitationByToken
|
||||
export const GetActiveInvitationByToken = async (invitation_token) => {
|
||||
let invitation = await db.getActiveInvitationByToken(invitation_token);
|
||||
return invitation;
|
||||
};
|
||||
|
||||
//updateInvitationStatusToAccepted
|
||||
export const UpdateInvitationStatusToAccepted = async (invitation_token) => {
|
||||
return await db.updateInvitationStatusToAccepted(invitation_token);
|
||||
};
|
||||
|
||||
//getUserById
|
||||
export const GetUserByID = async (userID) => {
|
||||
return await db.getUserById(userID);
|
||||
};
|
||||
|
||||
//getUserByEmail
|
||||
export const GetUserByEmail = async (email) => {
|
||||
return await db.getUserByEmail(email);
|
||||
};
|
||||
|
||||
export const SendEmailWithTemplate = async (template, data, email, subject, emailText) => {
|
||||
//for each key in data, replace the key in template with value
|
||||
for (const key in data) {
|
||||
if (Object.hasOwnProperty.call(data, key)) {
|
||||
const value = data[key];
|
||||
template = ReplaceAllOccurrences(template, `{{${key}}}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
const senderEmail = process.env.RESEND_SENDER_EMAIL;
|
||||
const resendKey = process.env.RESEND_API_KEY;
|
||||
|
||||
let mail = {
|
||||
from: senderEmail,
|
||||
to: [email],
|
||||
subject: subject,
|
||||
text: emailText,
|
||||
html: template,
|
||||
};
|
||||
|
||||
let smtpData = GetSMTPFromENV();
|
||||
|
||||
try {
|
||||
if (!!smtpData) {
|
||||
const transporter = getSMTPTransport(smtpData);
|
||||
const mailOptions = {
|
||||
from: smtpData.smtp_from_email,
|
||||
to: email,
|
||||
subject: mail.subject,
|
||||
html: mail.html,
|
||||
text: mail.text,
|
||||
};
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} else {
|
||||
const resend = new Resend(resendKey);
|
||||
return await resend.emails.send(mail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
throw new Error("Error sending email v");
|
||||
}
|
||||
};
|
||||
|
||||
export const GetSiteLogoURL = async (siteURL, logo, base) => {
|
||||
if (logo.startsWith("http")) {
|
||||
return logo;
|
||||
}
|
||||
return siteURL + base + logo;
|
||||
};
|
||||
export const GetAllUsers = async () => {
|
||||
return await db.getAllUsers();
|
||||
};
|
||||
|
||||
//get all users paginated
|
||||
export const GetAllUsersPaginated = async (data) => {
|
||||
return await db.getUsersPaginated(data.page, data.limit);
|
||||
};
|
||||
|
||||
//given a limit return total pages
|
||||
export const GetTotalUserPages = async (limit) => {
|
||||
let totalUsers = await db.getTotalUsers();
|
||||
let totalPages = Math.ceil(totalUsers.count / limit);
|
||||
return totalPages;
|
||||
};
|
||||
|
||||
export const ValidatePassword = (password) => {
|
||||
return /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/.test(password);
|
||||
};
|
||||
|
||||
export const UpdatePassword = async (data) => {
|
||||
let { userID, newPassword, newPlainPassword } = data;
|
||||
if (!ValidatePassword(newPassword)) {
|
||||
throw new Error(
|
||||
"Password must contain at least 8 characters, one uppercase letter, one lowercase letter and one number",
|
||||
);
|
||||
}
|
||||
// newPassword should match newPlainPassword
|
||||
if (newPassword !== newPlainPassword) {
|
||||
throw new Error("Passwords do not match");
|
||||
}
|
||||
|
||||
//hash the password
|
||||
let hashedPassword = await HashPassword(newPassword);
|
||||
|
||||
return await db.updateUserPassword({
|
||||
id: userID,
|
||||
password_hash: hashedPassword,
|
||||
});
|
||||
};
|
||||
|
||||
export const ManualUpdateUserData = async (byUser, forUserId, data) => {
|
||||
let forUser = await db.getUserById(forUserId);
|
||||
//only admins can update
|
||||
if (byUser.role !== "admin") {
|
||||
throw new Error("You do not have permission to update user");
|
||||
}
|
||||
if (data.updateType == "role") {
|
||||
return await db.updateUserRole(forUser.id, data.role);
|
||||
} else if (data.updateType == "is_active") {
|
||||
return await db.updateUserIsActive(forUser.id, data.is_active);
|
||||
} else if (data.updateType == "password") {
|
||||
return await UpdatePassword({
|
||||
userID: forUser.id,
|
||||
newPassword: data.password,
|
||||
newPlainPassword: data.passwordPlain,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const CreateNewUser = async (currentUser, data) => {
|
||||
let acceptedRoles = ["member", "editor"];
|
||||
if (!acceptedRoles.includes(data.role)) {
|
||||
throw new Error("Invalid role");
|
||||
}
|
||||
|
||||
if (currentUser.role === "member") {
|
||||
throw new Error("Only admins and editors can create new users");
|
||||
}
|
||||
|
||||
//if data.email empty, throw error
|
||||
if (!!!data.email) {
|
||||
throw new Error("Email cannot be empty");
|
||||
}
|
||||
|
||||
//if data.name empty, throw error
|
||||
if (!!!data.name) {
|
||||
throw new Error("Name cannot be empty");
|
||||
}
|
||||
|
||||
//if data.password empty, throw error
|
||||
if (!!!data.password) {
|
||||
throw new Error("Password cannot be empty");
|
||||
}
|
||||
|
||||
//if data.role empty, throw error
|
||||
if (!!!data.role) {
|
||||
throw new Error("Role cannot be empty");
|
||||
}
|
||||
|
||||
//if data.password not equal to data.plainPassword, throw error
|
||||
if (data.password !== data.plainPassword) {
|
||||
throw new Error("Passwords do not match");
|
||||
}
|
||||
|
||||
if (!ValidatePassword(data.password)) {
|
||||
throw new Error(
|
||||
"Password must contain at least 8 characters, one uppercase letter, one lowercase letter and one number",
|
||||
);
|
||||
}
|
||||
let user = {
|
||||
email: data.email,
|
||||
password_hash: await HashPassword(data.password),
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
};
|
||||
return await db.insertUser(user);
|
||||
};
|
||||
|
||||
export const GetSiteMap = async (cookies) => {
|
||||
let siteMapData = [];
|
||||
let siteURLData = await GetSiteDataByKey("siteURL");
|
||||
|
||||
@@ -66,90 +66,100 @@ async function manualIncident(monitor) {
|
||||
}
|
||||
|
||||
const Minuter = async (monitor) => {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
try {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type,
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type,
|
||||
});
|
||||
cb();
|
||||
});
|
||||
cb();
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "TCP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "DNS") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
if (monitor.default_status !== undefined && monitor.default_status !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.default_status,
|
||||
latency: 0,
|
||||
type: DEFAULT_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in realTimeData) {
|
||||
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "TCP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "DNS") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Monitor name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
if (monitor.default_status !== undefined && monitor.default_status !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.default_status,
|
||||
latency: 0,
|
||||
type: DEFAULT_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in realTimeData) {
|
||||
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
alertingQueue.push(async (cb) => {
|
||||
setTimeout(async () => {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
try {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Alerting name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
cb();
|
||||
}
|
||||
}, 1042);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,6 +30,14 @@ class DbImpl {
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
//given monitor_tags array of string, start and end timestamp in utc seconds return data
|
||||
async getMonitoringDataAll(monitor_tags, start, end) {
|
||||
return await this.knex("monitoring_data")
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.where("timestamp", ">=", start)
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
|
||||
//get latest data for a monitor_tag
|
||||
async getLatestMonitoringData(monitor_tag) {
|
||||
@@ -40,6 +48,38 @@ class DbImpl {
|
||||
.first();
|
||||
}
|
||||
|
||||
//get latest data for all active monitors
|
||||
async getLatestMonitoringDataAllActive(monitor_tags) {
|
||||
// Find the latest timestamp for each provided monitor tag
|
||||
const latestTimestamps = await this.knex("monitoring_data")
|
||||
.select("monitor_tag")
|
||||
.select(this.knex.raw("MAX(timestamp) as max_timestamp"))
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.groupBy("monitor_tag");
|
||||
|
||||
// Early exit if no results
|
||||
if (!latestTimestamps || latestTimestamps.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Then fetch the complete records using the timestamp pairs
|
||||
const conditions = latestTimestamps.map((item) => {
|
||||
return function () {
|
||||
this.where(function () {
|
||||
this.where("monitor_tag", item.monitor_tag).andWhere("timestamp", item.max_timestamp);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Build query with OR conditions
|
||||
let query = this.knex("monitoring_data").where(conditions[0]);
|
||||
for (let i = 1; i < conditions.length; i++) {
|
||||
query = query.orWhere(conditions[i]);
|
||||
}
|
||||
|
||||
return await query;
|
||||
}
|
||||
|
||||
async getLastHeartbeat(monitor_tag) {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
@@ -75,6 +115,15 @@ class DbImpl {
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
//get the last status before the timestamp given monitor_tag and start timestamp
|
||||
async getLastStatusBeforeAll(monitor_tags, timestamp) {
|
||||
return await this.knex("monitoring_data")
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.where("timestamp", "<", timestamp)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
async getDataGroupByDayAlternative(monitor_tag, start, end) {
|
||||
// Fetch all raw data
|
||||
@@ -403,6 +452,14 @@ class DbImpl {
|
||||
return await this.knex("users").select("password_hash").where("id", id).first();
|
||||
}
|
||||
|
||||
//get user name, email from users given id
|
||||
async getUserById(id) {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "is_active", "is_verified", "role", "created_at", "updated_at")
|
||||
.where("id", id)
|
||||
.first();
|
||||
}
|
||||
|
||||
//insert user
|
||||
async insertUser(data) {
|
||||
return await this.knex("users").insert({
|
||||
@@ -423,6 +480,27 @@ class DbImpl {
|
||||
});
|
||||
}
|
||||
|
||||
//get all columns of users except password_hash order by create at
|
||||
async getAllUsers() {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "role", "is_active", "is_verified", "created_at", "updated_at")
|
||||
.orderBy("created_at", "desc");
|
||||
}
|
||||
|
||||
//get all users paginated
|
||||
async getUsersPaginated(page, limit) {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "role", "is_active", "is_verified", "created_at", "updated_at")
|
||||
.orderBy("created_at", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
}
|
||||
|
||||
//getTotalUsers
|
||||
async getTotalUsers() {
|
||||
return await this.knex("users").count("* as count").first();
|
||||
}
|
||||
|
||||
//new api key
|
||||
async createNewApiKey(data) {
|
||||
return await this.knex("api_keys").insert({
|
||||
@@ -742,6 +820,97 @@ class DbImpl {
|
||||
async getIncidentCommentByID(id) {
|
||||
return await this.knex("incident_comments").where({ id }).first();
|
||||
}
|
||||
|
||||
//update users.name by id
|
||||
async updateUserName(id, name) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
name,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
//updateUserRole
|
||||
async updateUserRole(id, role) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
role,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update is_active
|
||||
async updateUserIsActive(id, is_active) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
is_active,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update password
|
||||
async updateUserPassword(data) {
|
||||
return await this.knex("users").where({ id: data.id }).update({
|
||||
password_hash: data.password_hash,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async updateIsVerified(id, is_verified) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
is_verified: is_verified,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//insert into invitations
|
||||
async insertInvitation(data) {
|
||||
return await this.knex("invitations").insert({
|
||||
invitation_token: data.invitation_token,
|
||||
invitation_type: data.invitation_type,
|
||||
invited_user_id: data.invited_user_id,
|
||||
invited_by_user_id: data.invited_by_user_id,
|
||||
invitation_meta: data.invitation_meta,
|
||||
invitation_expiry: data.invitation_expiry,
|
||||
invitation_status: data.invitation_status,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update invitation_status of all invitations for invited_user_id, given invitation_type to VOID
|
||||
async updateInvitationStatusToVoid(invited_user_id, invitation_type) {
|
||||
return await this.knex("invitations").where({ invited_user_id, invitation_type }).update({
|
||||
invitation_status: "VOID",
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//check if there is a row for given invited_user_id,invitation_type and invitation_status = PENDING
|
||||
async invitationExists(invited_user_id, invitation_type) {
|
||||
const result = await this.knex("invitations")
|
||||
.count("* as count")
|
||||
.where({
|
||||
invited_user_id,
|
||||
invitation_type,
|
||||
invitation_status: "PENDING",
|
||||
})
|
||||
.first();
|
||||
return result.count > 0;
|
||||
}
|
||||
|
||||
//update invitation_status of invitation given invitation_token to ACCEPTED
|
||||
async updateInvitationStatusToAccepted(invitation_token) {
|
||||
return await this.knex("invitations").where({ invitation_token }).update({
|
||||
invitation_status: "ACCEPTED",
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//get invitations by token, the invitation should be PENDING, and it should not be expired
|
||||
async getActiveInvitationByToken(invitation_token) {
|
||||
return await this.knex("invitations")
|
||||
.where("invitation_token", invitation_token)
|
||||
.andWhere("invitation_status", "PENDING")
|
||||
.andWhere("invitation_expiry", ">", this.knex.fn.now())
|
||||
.first();
|
||||
}
|
||||
}
|
||||
|
||||
export default DbImpl;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import Mustache from "mustache";
|
||||
import { DiscordJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Discord {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
@@ -17,77 +24,43 @@ class Discord {
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.discord_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.discord_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.discord_body,
|
||||
secret.find,
|
||||
secret.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let siteURL = this.siteData.siteURL;
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
//remove / if there at the end of the url
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
let discordTemplate = DiscordJSONTemplate;
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
discordTemplate = this.trigger_meta.discord_body;
|
||||
}
|
||||
|
||||
if (!this.siteData.logo.startsWith("http"))
|
||||
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
|
||||
|
||||
let logo = this.siteData.logo;
|
||||
|
||||
let color = 13250616; //down;
|
||||
if (data.severity === "warning") {
|
||||
color = 15125089;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
color = 5156244;
|
||||
}
|
||||
return {
|
||||
username: this.siteData.siteName,
|
||||
avatar_url: logo,
|
||||
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
|
||||
embeds: [
|
||||
{
|
||||
title: data.alert_name,
|
||||
description: data.description,
|
||||
url: data.actions[0].url,
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: "Monitor",
|
||||
value: data.details.metric,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Alert ID",
|
||||
value: data.id,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Current Value",
|
||||
value: data.details.current_value,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Threshold",
|
||||
value: data.details.threshold,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: "Kener",
|
||||
icon_url: logo,
|
||||
},
|
||||
timestamp: data.timestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
return Mustache.render(discordTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const toSend = this.transformData(data);
|
||||
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data)),
|
||||
body: toSend,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from discord`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
@@ -3,56 +3,57 @@ import { Resend } from "resend";
|
||||
import nodemailer from "nodemailer";
|
||||
import getSMTPTransport from "./smtps.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Email {
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
|
||||
let metaString = JSON.stringify(meta);
|
||||
let metaString = JSON.stringify(meta);
|
||||
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: ""
|
||||
};
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: "",
|
||||
};
|
||||
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
|
||||
let html = `
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -177,53 +178,53 @@ class Email {
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system.
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system. It is being powered by <a href='https://kener.ing'>Kener</a> v${version()}.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
emailApiRequest.html = html;
|
||||
emailApiRequest.html = html;
|
||||
|
||||
return emailApiRequest;
|
||||
}
|
||||
return emailApiRequest;
|
||||
}
|
||||
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html // HTML body (if any)
|
||||
};
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html, // HTML body (if any)
|
||||
};
|
||||
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Email;
|
||||
|
||||
@@ -5,26 +5,26 @@ import Slack from "./slack.js";
|
||||
import Email from "./email.js";
|
||||
|
||||
class Notification {
|
||||
client;
|
||||
client;
|
||||
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
}
|
||||
export default Notification;
|
||||
|
||||
@@ -1,100 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import variables from "./variables.js";
|
||||
import { SlackJSONTemplate } from "../../anywhere.js";
|
||||
import Mustache from "mustache";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Slack {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener"
|
||||
};
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
}
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
transformData(alert) {
|
||||
return {
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.alert_name,
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${alert.description}\n*Source:* ${alert.source}\n*Severity:* ${alert.severity}\n*Status:* ${alert.status}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Metric:*\n${alert.details.metric}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Current Value:*\n${alert.details.current_value}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Threshold:*\n${alert.details.threshold}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Timestamp:*\n<!date^${Math.floor(new Date(alert.timestamp).getTime() / 1000)}^{date} at {time}|${alert.timestamp}>`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: alert.actions.map((action) => ({
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: action.text
|
||||
},
|
||||
url: action.url,
|
||||
style: "primary"
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.slack_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.slack_body = ReplaceAllOccurrences(this.trigger_meta.slack_body, secret.find, secret.replace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data))
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
transformData(alert) {
|
||||
let view = variables(this.siteData, alert);
|
||||
|
||||
let slackTemplate = SlackJSONTemplate;
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
slackTemplate = this.trigger_meta.slack_body;
|
||||
}
|
||||
return Mustache.render(slackTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from slack`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Slack;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import nodemailer from "nodemailer";
|
||||
import { HashString } from "../controllers/controller.js";
|
||||
import { HashString } from "../tool.js";
|
||||
const transports = {};
|
||||
export default function getSMTPTransport(meta) {
|
||||
//convert meta to string and generate has id
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
export default function variables(siteData, data) {
|
||||
let siteURL = siteData.siteURL;
|
||||
let siteName = siteData.siteName;
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!siteData.logo.startsWith("http")) {
|
||||
siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${siteData.logo}`;
|
||||
}
|
||||
let view = {
|
||||
site_name: siteName,
|
||||
logo_url: siteData.logo,
|
||||
site_url: siteURL,
|
||||
alert_name: data.alert_name,
|
||||
status: data.status,
|
||||
is_resolved: data.status === "RESOLVED",
|
||||
is_triggered: data.status === "TRIGGERED",
|
||||
description: data.description,
|
||||
action_text: data.actions[0].text,
|
||||
action_url: data.actions[0].url,
|
||||
metric: data.details.metric,
|
||||
severity: data.severity,
|
||||
id: data.id,
|
||||
current_value: data.details.current_value,
|
||||
threshold: data.details.threshold,
|
||||
source: data.source,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import Mustache from "mustache";
|
||||
import { WebhookJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Webhook {
|
||||
url;
|
||||
@@ -13,7 +17,7 @@ class Webhook {
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener/3.2.5",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
@@ -52,8 +56,13 @@ class Webhook {
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
|
||||
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
if (!!!this.trigger_meta.has_webhook_body || !!!this.trigger_meta.webhook_body) {
|
||||
return Mustache.render(WebhookJSONTemplate, view);
|
||||
}
|
||||
|
||||
//have to keep this for backward compatibility
|
||||
let body = this.trigger_meta.webhook_body;
|
||||
body = ReplaceAllOccurrences(body, "${id}", data.id);
|
||||
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
|
||||
@@ -67,7 +76,9 @@ class Webhook {
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
return body;
|
||||
//have to keep this for backward compatibility
|
||||
|
||||
return Mustache.render(body, view);
|
||||
}
|
||||
|
||||
type() {
|
||||
@@ -81,6 +92,9 @@ class Webhook {
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from webhook`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
@@ -3,20 +3,9 @@ import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
import { DefaultAPIEval } from "../../anywhere.js";
|
||||
import version from "../../version.js";
|
||||
import https from "https";
|
||||
|
||||
class ApiCall {
|
||||
monitor;
|
||||
@@ -31,7 +20,7 @@ class ApiCall {
|
||||
|
||||
async execute() {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/" + "3.1.0";
|
||||
axiosHeaders["User-Agent"] = `Kener/${version()}`;
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
|
||||
let body = this.monitor.type_data.body;
|
||||
@@ -44,10 +33,10 @@ class ApiCall {
|
||||
}
|
||||
|
||||
let method = this.monitor.type_data.method;
|
||||
let timeout = this.monitor.type_data.timeout || 5000;
|
||||
let timeout = this.monitor.type_data.timeout || 10000;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : defaultEval;
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultAPIEval;
|
||||
|
||||
for (let i = 0; i < this.envSecrets.length; i++) {
|
||||
const secret = this.envSecrets[i];
|
||||
@@ -82,6 +71,10 @@ class ApiCall {
|
||||
transformResponse: (r) => r,
|
||||
};
|
||||
|
||||
if (!!this.monitor.type_data.allowSelfSignedCert) {
|
||||
options.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
if (!!body) {
|
||||
options.data = body;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences, Wait } from "../tool.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences, Wait, GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import db from "../db/db.js";
|
||||
|
||||
@@ -29,6 +29,9 @@ class GroupCall {
|
||||
}
|
||||
|
||||
async execute(startOfMinute) {
|
||||
if (!!!startOfMinute) {
|
||||
startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
}
|
||||
let tagArr = this.monitor.type_data.monitors.map((m) => m.tag);
|
||||
return await waitForDataAndReturn(tagArr, startOfMinute, 500, this.monitor.type_data.timeout);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { Ping } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultPingEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { DefaultPingEval } from "../../anywhere.js";
|
||||
|
||||
class PingCall {
|
||||
monitor;
|
||||
@@ -27,7 +13,7 @@ class PingCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : defaultPingEval;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : DefaultPingEval;
|
||||
let tag = this.monitor.tag;
|
||||
if (hosts === undefined) {
|
||||
console.log(
|
||||
|
||||
@@ -2,25 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { TCP } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultTcpEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { DefaultTCPEval } from "../../anywhere.js";
|
||||
|
||||
class TcpCall {
|
||||
monitor;
|
||||
@@ -31,7 +13,7 @@ class TcpCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : defaultTcpEval;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : DefaultTCPEval;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
if (hosts === undefined) {
|
||||
|
||||
@@ -4,29 +4,17 @@ import figlet from "figlet";
|
||||
import { Cron } from "croner";
|
||||
import { Minuter } from "./cron-minute.js";
|
||||
import db from "./db/db.js";
|
||||
import { GetAllSiteData, GetMonitorsParsed, HashString } from "./controllers/controller.js";
|
||||
import { GetAllSiteData, GetMonitorsParsed } from "./controllers/controller.js";
|
||||
import { HashString } from "./tool.js";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
import version from "../version.js";
|
||||
|
||||
const jobs = [];
|
||||
process.env.TZ = "UTC";
|
||||
let isStartUP = true;
|
||||
|
||||
// Get the version from package.json
|
||||
const getVersion = () => {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = resolve(__dirname, "../../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
console.error("Error reading version:", error);
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleCronJobs = async () => {
|
||||
// Fetch and map all active monitors, creating a unique hash for each
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((monitor) => ({
|
||||
@@ -85,15 +73,13 @@ async function Startup() {
|
||||
|
||||
mainJob.trigger();
|
||||
|
||||
const version = getVersion();
|
||||
|
||||
figlet("Kener v" + version, function (err, data) {
|
||||
figlet("Kener v" + version(), function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
console.log(`Kener version ${version} is running!`);
|
||||
console.log(`Kener version ${version()} is running!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--$-->
|
||||
</head>
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Reset your {{brand_name}} password
|
||||
</div>
|
||||
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
max-width: 600px;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 20px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="text-align: center"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}} Logo"
|
||||
height="50"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="200"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
"
|
||||
>
|
||||
Password Reset Request
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Hello,
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
We received a request to reset your password for your {{brand_name}} account. If you
|
||||
didn't make this request, you can safely ignore this email.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
To reset your password, please click the button below:
|
||||
</p>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="text-align: center; margin-bottom: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{link}}"
|
||||
style="
|
||||
background-color: rgb(37, 99, 235);
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: 700;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>Reset Password</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
This password reset link will expire in 24 hours. If you need assistance, please
|
||||
contact our support team.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Thank you,<br />The {{brand_name}} Team
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--$-->
|
||||
</head>
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Verify your email address for {{brand_name}}
|
||||
</div>
|
||||
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
var(--tw-ring-offset-shadow, 0 0 #0000),
|
||||
var(--tw-ring-shadow, 0 0 #0000),
|
||||
0 1px 2px 0 rgb(0, 0, 0, 0.05);
|
||||
max-width: 37.5em;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
height="auto"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="120"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: rgb(31, 41, 55);
|
||||
"
|
||||
>
|
||||
Verify Your Email Address
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Thank you for signing up with
|
||||
<!-- -->{{brand_name}}<!-- -->! To complete your registration and get started,
|
||||
please verify your email address.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Click the button below to verify your email address and activate your account:
|
||||
</p>
|
||||
<a
|
||||
href="{{verification_url}}"
|
||||
style="
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
text-decoration-line: none;
|
||||
text-align: center;
|
||||
color: rgb(255, 255, 255);
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
background-color: #ed702d;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>Verify Email Address</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
margin-top: 32px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
If you didn't create an account with
|
||||
<!-- -->{{brand_name}}<!-- -->, you can safely ignore this email.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
If the button above doesn't work, copy and paste the following link into
|
||||
your browser:
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
word-break: break-all;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
<a
|
||||
href="{{verification_url}}"
|
||||
style="color: rgb(37, 99, 235); text-decoration-line: underline"
|
||||
target="_blank"
|
||||
>{{verification_url}}</a
|
||||
>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { AllRecordTypes } from "./constants.js";
|
||||
import knexOb from "../../../knexfile.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
@@ -286,6 +287,11 @@ function GetDbType() {
|
||||
//sqlite, postgresql, mysql
|
||||
return knexOb.databaseType;
|
||||
}
|
||||
function HashString(str) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(str);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export {
|
||||
IsValidURL,
|
||||
@@ -315,4 +321,5 @@ export {
|
||||
Wait,
|
||||
MaskString,
|
||||
GetDbType,
|
||||
HashString,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Component, Input, ElementRef, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-embed-monitor',
|
||||
template: `<div [id]="containerId"></div>`
|
||||
})
|
||||
export class EmbedMonitorComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@Input() monitor!: string;
|
||||
@Input() theme: string = "light";
|
||||
@Input() bgc: string = "transparent";
|
||||
@Input() locale: string = "en";
|
||||
|
||||
containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
private iframe: HTMLIFrameElement | null = null;
|
||||
private messageListener!: (event: MessageEvent) => void;
|
||||
|
||||
constructor(private el: ElementRef) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.createIframe();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.createIframe();
|
||||
}
|
||||
|
||||
createIframe() {
|
||||
const container = this.el.nativeElement.querySelector(`#${this.containerId}`);
|
||||
if (!this.monitor || !container) return;
|
||||
|
||||
// Remove previous iframe if it exists
|
||||
if (this.iframe) {
|
||||
this.iframe.remove();
|
||||
}
|
||||
|
||||
this.iframe = document.createElement("iframe");
|
||||
this.iframe.src = `${this.monitor}?theme=${this.theme}&bgc=${this.bgc}&locale=${this.locale}`;
|
||||
this.iframe.width = "0%";
|
||||
this.iframe.height = "0";
|
||||
this.iframe.frameBorder = "0";
|
||||
this.iframe.allowTransparency = true;
|
||||
this.iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
this.iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(this.iframe);
|
||||
|
||||
this.messageListener = (event: MessageEvent) => {
|
||||
if (event.data?.height) this.iframe!.height = event.data.height;
|
||||
if (event.data?.width) this.iframe!.width = event.data.width;
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.messageListener);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.removeEventListener("message", this.messageListener);
|
||||
if (this.iframe) {
|
||||
this.iframe.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<app-embed-monitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"></app-embed-monitor>
|
||||
@@ -0,0 +1,8 @@
|
||||
<iframe
|
||||
src="http://localhost:3000/embed/monitor-{selectedMonitor.value}?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
@@ -0,0 +1 @@
|
||||
<script async src="http://localhost:3000/embed/monitor-{selectedMonitor.value}/js?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}&monitor=http://localhost:3000/embed/monitor-{selectedMonitor.value}"></script>
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const EmbedMonitor = ({ monitor, theme = "light", bgc = "transparent", locale = "en" }) => {
|
||||
const containerRef = useRef(null);
|
||||
const iframeRef = useRef(null);
|
||||
const [uid] = useState(`embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monitor || !containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Remove previous iframe if exists
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.remove();
|
||||
}
|
||||
|
||||
// Create a new iframe
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
iframe.width = "0%";
|
||||
iframe.height = "0";
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allowTransparency = true;
|
||||
iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(iframe);
|
||||
iframeRef.current = iframe;
|
||||
|
||||
const setHeight = (data) => {
|
||||
if (data.height !== undefined) {
|
||||
iframe.height = data.height;
|
||||
}
|
||||
};
|
||||
|
||||
const setWidth = (data) => {
|
||||
if (data.width !== undefined) {
|
||||
iframe.width = data.width;
|
||||
}
|
||||
};
|
||||
|
||||
const messageListener = (event) => {
|
||||
if (event.data && event.data.height !== undefined) {
|
||||
setHeight(event.data);
|
||||
}
|
||||
if (event.data && event.data.width !== undefined) {
|
||||
setWidth(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
if (iframe) {
|
||||
iframe.remove();
|
||||
}
|
||||
};
|
||||
}, [monitor, theme, bgc, locale]);
|
||||
|
||||
return <div id={uid} ref={containerRef}></div>;
|
||||
};
|
||||
|
||||
export default EmbedMonitor;
|
||||
@@ -0,0 +1,12 @@
|
||||
import EmbedMonitor from "./EmbedMonitor";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<h2>React Embed</h2>
|
||||
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import { onMount, onDestroy, beforeUpdate } from "svelte";
|
||||
|
||||
export let monitor;
|
||||
export let theme = "light";
|
||||
export let bgc = "transparent";
|
||||
export let locale = "en";
|
||||
|
||||
let iframe, listeners;
|
||||
let containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
let isMounted = false;
|
||||
let prevMonitor, prevTheme, prevBgc, prevLocale;
|
||||
|
||||
onMount(() => {
|
||||
if (!monitor) return;
|
||||
|
||||
isMounted = true;
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const uid = "KENER_" + ~~(new Date().getTime() / 86400000);
|
||||
const uriEmbedded = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.src = uriEmbedded;
|
||||
iframe.id = uid;
|
||||
iframe.width = "0%";
|
||||
iframe.height = "0";
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allowTransparency = true;
|
||||
iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(iframe);
|
||||
|
||||
const setHeight = (data) => {
|
||||
if (data.height !== undefined) {
|
||||
iframe.height = data.height;
|
||||
}
|
||||
};
|
||||
|
||||
const setWidth = (data) => {
|
||||
if (data.width !== undefined) {
|
||||
iframe.width = data.width;
|
||||
}
|
||||
};
|
||||
|
||||
listeners = (event) => {
|
||||
if (event.data && event.data.height !== undefined) {
|
||||
setHeight(event.data);
|
||||
}
|
||||
if (event.data && event.data.width !== undefined) {
|
||||
setWidth(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", listeners, false);
|
||||
});
|
||||
|
||||
beforeUpdate(() => {
|
||||
if (
|
||||
isMounted &&
|
||||
iframe &&
|
||||
(monitor !== prevMonitor || theme !== prevTheme || bgc !== prevBgc || locale !== prevLocale)
|
||||
) {
|
||||
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
// Update previous values
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (iframe) {
|
||||
window.removeEventListener("message", listeners);
|
||||
iframe.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dynamic container with unique ID -->
|
||||
<div id={containerId}></div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import EmbedMonitor from "./EmbedMonitor.svelte";
|
||||
</script>
|
||||
|
||||
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :id="containerId" ref="containerRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watchEffect, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
monitor: String,
|
||||
theme: { type: String, default: "light" },
|
||||
bgc: { type: String, default: "transparent" },
|
||||
locale: { type: String, default: "en" }
|
||||
},
|
||||
setup(props) {
|
||||
const containerRef = ref(null);
|
||||
const iframeRef = ref(null);
|
||||
const containerId = ref(`embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`);
|
||||
|
||||
let messageListener = null;
|
||||
|
||||
const createIframe = () => {
|
||||
if (!props.monitor || !containerRef.value) return;
|
||||
|
||||
// Remove existing iframe if any
|
||||
if (iframeRef.value) {
|
||||
iframeRef.value.remove();
|
||||
}
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = `${props.monitor}?theme=${props.theme}&bgc=${props.bgc}&locale=${props.locale}`;
|
||||
iframe.width = "0%";
|
||||
iframe.height = "0";
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allowTransparency = true;
|
||||
iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
containerRef.value.appendChild(iframe);
|
||||
iframeRef.value = iframe;
|
||||
|
||||
messageListener = (event) => {
|
||||
if (event.data?.height) iframe.height = event.data.height;
|
||||
if (event.data?.width) iframe.width = event.data.width;
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageListener);
|
||||
};
|
||||
|
||||
onMounted(createIframe);
|
||||
|
||||
watchEffect(() => {
|
||||
createIframe();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
if (iframeRef.value) {
|
||||
iframeRef.value.remove();
|
||||
}
|
||||
});
|
||||
|
||||
return { containerRef, containerId };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Vue Embed</h2>
|
||||
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmbedMonitor from "./EmbedMonitor.vue";
|
||||
|
||||
export default {
|
||||
components: { EmbedMonitor }
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
|
||||
function getVersionUsingVite() {
|
||||
try {
|
||||
if (!!import.meta.env.PACKAGE_VERSION) {
|
||||
return import.meta.env.PACKAGE_VERSION;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function version() {
|
||||
let v = getVersionUsingVite();
|
||||
if (!!v) {
|
||||
return v;
|
||||
} else {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = resolve(__dirname, "../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch (e) {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
}
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { marked } from "marked";
|
||||
import version from "$lib/version.js";
|
||||
|
||||
function extractMetadataAndContent(fileContent) {
|
||||
// Regular expression to match metadata section
|
||||
@@ -15,9 +16,7 @@ function extractMetadataAndContent(fileContent) {
|
||||
|
||||
if (match) {
|
||||
// Extract metadata section and remaining content
|
||||
const metadataLines = match[1]
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
const metadataLines = match[1].split("\n").filter((line) => line.trim() !== "");
|
||||
result.content = fileContent.slice(match[0].length).trim(); // Remaining content after metadata
|
||||
|
||||
metadataLines.forEach((line) => {
|
||||
@@ -40,10 +39,7 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
docFilePath = docFolderPath + `/${docFile}.md`;
|
||||
}
|
||||
const fileContents = await fs.readFileSync(docFilePath, "utf-8");
|
||||
const siteStructure = await fs.readFileSync(
|
||||
docFolderPath + "/structure.json",
|
||||
"utf-8",
|
||||
);
|
||||
const siteStructure = await fs.readFileSync(docFolderPath + "/structure.json", "utf-8");
|
||||
const { metadata, content } = extractMetadataAndContent(fileContents);
|
||||
|
||||
const selectedDoc = docFilePath.replace(docFolderPath, "");
|
||||
@@ -54,5 +50,6 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
title: metadata.title || "Kener Docs",
|
||||
description: metadata.description || "Kener Docs",
|
||||
siteStructure: siteStructureJSON,
|
||||
kenerVersion: version(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
let defaultTheme = "light";
|
||||
export let data;
|
||||
let siteStructure = data.siteStructure;
|
||||
@@ -110,7 +111,9 @@
|
||||
<!-- Document Icon - Replace with your own logo -->
|
||||
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
|
||||
<span class="text-xl font-medium">Kener Documentation</span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.5 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium">
|
||||
{$page.data.kenerVersion}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-svelte";
|
||||
import ArrowLeft from "lucide-svelte/icons/arrow-left";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
export let data;
|
||||
let subHeadings = [];
|
||||
@@ -65,21 +67,28 @@
|
||||
//function to find next and previous path
|
||||
function findNextAndPreviousPath() {
|
||||
let structure = data.siteStructure.sidebar;
|
||||
for (let i = 0; i < structure.length; i++) {
|
||||
let children = structure[i].children;
|
||||
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
if (children[j].file === data.docFilePath) {
|
||||
if (j > 0) {
|
||||
previousPath = children[j - 1];
|
||||
}
|
||||
if (j < children.length - 1) {
|
||||
nextPath = children[j + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Flatten the structure - combine all children arrays into one
|
||||
const flattenedPages = [];
|
||||
|
||||
for (let i = 0; i < structure.length; i++) {
|
||||
const section = structure[i];
|
||||
if (section.children && Array.isArray(section.children)) {
|
||||
flattenedPages.push(...section.children);
|
||||
}
|
||||
}
|
||||
|
||||
// Find current page index in the flattened array
|
||||
const currentIndex = flattenedPages.findIndex((page) => page.file === data.docFilePath);
|
||||
|
||||
// Set previous and next paths if they exist
|
||||
if (currentIndex > 0) {
|
||||
previousPath = flattenedPages[currentIndex - 1];
|
||||
}
|
||||
|
||||
if (currentIndex < flattenedPages.length - 1) {
|
||||
nextPath = flattenedPages[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
|
||||
afterNavigate(() => {
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
<script>
|
||||
import "../../app.postcss";
|
||||
import "../../kener.css";
|
||||
import Nav from "$lib/components/nav.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Sun from "lucide-svelte/icons/sun";
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import { Languages } from "lucide-svelte";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { setMode, mode, ModeWatcher } from "mode-watcher";
|
||||
export let data;
|
||||
import "../../app.postcss";
|
||||
import "../../kener.css";
|
||||
import Nav from "$lib/components/nav.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Sun from "lucide-svelte/icons/sun";
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import Languages from "lucide-svelte/icons/languages";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { setMode, mode, ModeWatcher } from "mode-watcher";
|
||||
export let data;
|
||||
|
||||
let defaultLocaleKey = data.selectedLang;
|
||||
let defaultTheme = data.site.theme;
|
||||
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
|
||||
let defaultLocaleKey = data.selectedLang;
|
||||
let defaultTheme = data.site.theme;
|
||||
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
|
||||
|
||||
function toggleMode() {
|
||||
if ($mode === "light") {
|
||||
setMode("dark");
|
||||
} else {
|
||||
setMode("light");
|
||||
}
|
||||
}
|
||||
let defaultLocaleValue;
|
||||
if (!allLocales) {
|
||||
defaultLocaleValue = "English";
|
||||
} else {
|
||||
defaultLocaleValue = allLocales.find((locale) => locale.code === defaultLocaleKey).name;
|
||||
}
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
function setLanguage(locale) {
|
||||
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
|
||||
if (locale === defaultLocaleKey) return;
|
||||
defaultLocaleValue = allLocales[locale];
|
||||
function toggleMode() {
|
||||
if ($mode === "light") {
|
||||
setMode("dark");
|
||||
} else {
|
||||
setMode("light");
|
||||
}
|
||||
}
|
||||
let defaultLocaleValue;
|
||||
if (!allLocales) {
|
||||
defaultLocaleValue = "English";
|
||||
} else {
|
||||
defaultLocaleValue = allLocales.find((locale) => locale.code === defaultLocaleKey).name;
|
||||
}
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
function setLanguage(locale) {
|
||||
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
|
||||
if (locale === defaultLocaleKey) return;
|
||||
defaultLocaleValue = allLocales[locale];
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
let Analytics;
|
||||
onMount(async () => {
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (localTz != data.localTz) {
|
||||
if (data.isBot === false) {
|
||||
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (defaultTheme != "none") {
|
||||
setMode(defaultTheme);
|
||||
}
|
||||
//
|
||||
});
|
||||
let Analytics;
|
||||
onMount(async () => {
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (localTz != data.localTz) {
|
||||
if (data.isBot === false) {
|
||||
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (defaultTheme != "none") {
|
||||
setMode(defaultTheme);
|
||||
}
|
||||
//
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.site.title}</title>
|
||||
{#if data.site.favicon && data.site.favicon[0] == "/"}
|
||||
<link rel="icon" id="kener-app-favicon" href="{base}{data.site.favicon}" />
|
||||
{:else if data.site.favicon}
|
||||
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
|
||||
{/if}
|
||||
<link href={data.site.font.cssSrc} rel="stylesheet" />
|
||||
{#each Object.entries(data.site.metaTags) as [key, value]}
|
||||
<meta name={key} content={value} />
|
||||
{/each}
|
||||
<title>{data.site.title}</title>
|
||||
{#if data.site.favicon && data.site.favicon[0] == "/"}
|
||||
<link rel="icon" id="kener-app-favicon" href="{base}{data.site.favicon}" />
|
||||
{:else if data.site.favicon}
|
||||
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
|
||||
{/if}
|
||||
<link href={data.site.font.cssSrc} rel="stylesheet" />
|
||||
{#each Object.entries(data.site.metaTags) as [key, value]}
|
||||
<meta name={key} content={value} />
|
||||
{/each}
|
||||
</svelte:head>
|
||||
<ModeWatcher />
|
||||
<main
|
||||
style="
|
||||
style="
|
||||
--font-family: {data.site.font.family};
|
||||
--bg-custom: {data.bgc};
|
||||
--up-color: {data.site.colors.UP};
|
||||
@@ -78,17 +76,17 @@
|
||||
--degraded-color: {data.site.colors.DEGRADED}
|
||||
"
|
||||
>
|
||||
<div class="">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
/* Apply the global font family using the CSS variable */
|
||||
* {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
main {
|
||||
background-color: var(--bg-custom);
|
||||
}
|
||||
/* Apply the global font family using the CSS variable */
|
||||
* {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
main {
|
||||
background-color: var(--bg-custom);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Sun from "lucide-svelte/icons/sun";
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import { Languages, Globe } from "lucide-svelte";
|
||||
import Languages from "lucide-svelte/icons/languages";
|
||||
import Globe from "lucide-svelte/icons/globe";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FetchData } from "$lib/server/page";
|
||||
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
|
||||
import { SortMonitor } from "$lib/clientTools.js";
|
||||
import moment from "moment";
|
||||
|
||||
function removeTags(str) {
|
||||
if (str === null || str === "") return false;
|
||||
else str = str.toString();
|
||||
@@ -29,7 +30,9 @@ async function returnTypeOfMonitorsPageMeta(url) {
|
||||
}
|
||||
|
||||
if (!!query.get("group")) {
|
||||
const groupStart = performance.now();
|
||||
let g = await GetMonitors({ status: "ACTIVE", tag: query.get("group") });
|
||||
|
||||
if (g.length > 0) {
|
||||
group = g[0];
|
||||
let typeData = JSON.parse(g[0].type_data);
|
||||
@@ -39,14 +42,20 @@ async function returnTypeOfMonitorsPageMeta(url) {
|
||||
}
|
||||
}
|
||||
|
||||
const monitorStart = performance.now();
|
||||
let monitors = await GetMonitors(filter);
|
||||
return { monitors, pageType, group };
|
||||
}
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
const totalLoadStart = performance.now();
|
||||
|
||||
const query = url.searchParams;
|
||||
|
||||
const metaStart = performance.now();
|
||||
let { monitors, pageType, group } = await returnTypeOfMonitorsPageMeta(url);
|
||||
|
||||
const processStart = performance.now();
|
||||
let hiddenGroupedMonitorsTags = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
if (pageType === "home" && monitors[i].monitor_type === "GROUP") {
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { l } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
import { ArrowRight, ChevronLeft, X } from "lucide-svelte";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
|
||||
import { onMount } from "svelte";
|
||||
import ShareMenu from "$lib/components/shareMenu.svelte";
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
// @ts-nocheck
|
||||
import { GetMonitors } from "$lib/server/controllers/controller.js";
|
||||
import { GetMonitors, GetLatestMonitoringData, GetLatestStatusActiveAll } from "$lib/server/controllers/controller.js";
|
||||
import StatusColor from "$lib/color.js";
|
||||
import { makeBadge } from "badge-maker";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { ErrorSvg } from "$lib/anywhere.js";
|
||||
|
||||
export async function GET({ params, setHeaders, url }) {
|
||||
// @ts-ignore
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const { tag } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
const lastObj = await db.getLatestMonitoringData(tag);
|
||||
let lastObj;
|
||||
let activeTags = monitors.map((monitor) => monitor.tag);
|
||||
if (params.tag == "_") {
|
||||
lastObj = await GetLatestStatusActiveAll(activeTags);
|
||||
} else {
|
||||
if (monitors.length === 0) {
|
||||
return new Response(ErrorSvg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
let m = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
if (!m) {
|
||||
return new Response(ErrorSvg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
lastObj = await GetLatestMonitoringData(params.tag);
|
||||
}
|
||||
//read query params
|
||||
const query = url.searchParams;
|
||||
const animate = query.get("animate") || "";
|
||||
|
||||
@@ -1,37 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import { GetMonitors } from "$lib/server/controllers/controller.js";
|
||||
import {
|
||||
GetMonitors,
|
||||
GetLatestMonitoringData,
|
||||
GetLatestStatusActiveAll,
|
||||
GetSiteDataByKey,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import StatusColor from "$lib/color.js";
|
||||
import { makeBadge } from "badge-maker";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { ErrorSvg } from "$lib/anywhere.js";
|
||||
|
||||
export async function GET({ params, setHeaders, url }) {
|
||||
// @ts-ignore
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const { tag, name } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
const lastObj = await db.getLatestMonitoringData(tag);
|
||||
let lastObj;
|
||||
let name;
|
||||
let tag;
|
||||
|
||||
//read query params
|
||||
let myColors = await StatusColor();
|
||||
const query = url.searchParams;
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
let label = query.get("label") || name;
|
||||
const color = query.get("color") || myColors[lastObj.status].substr(1);
|
||||
const style = query.get("style") || "flat";
|
||||
const siteName = await GetSiteDataByKey("siteName");
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
let activeTags = monitors.map((monitor) => monitor.tag);
|
||||
if (params.tag == "_") {
|
||||
lastObj = await GetLatestStatusActiveAll(activeTags);
|
||||
name = siteName;
|
||||
tag = "";
|
||||
} else {
|
||||
if (monitors.length === 0) {
|
||||
return new Response(ErrorSvg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
let m = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
tag = m.tag;
|
||||
name = m.name;
|
||||
lastObj = await GetLatestMonitoringData(tag);
|
||||
}
|
||||
|
||||
label = label.trim();
|
||||
//read query params
|
||||
let myColors = await StatusColor();
|
||||
const query = url.searchParams;
|
||||
const labelColor = query.get("labelColor") || "333";
|
||||
let label = query.get("label") || name;
|
||||
const color = query.get("color") || myColors[lastObj.status].substr(1);
|
||||
const style = query.get("style") || "flat";
|
||||
|
||||
const format = {
|
||||
label: label,
|
||||
message: lastObj.status,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
label = label.trim();
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml"
|
||||
}
|
||||
});
|
||||
const format = {
|
||||
label: label,
|
||||
message: lastObj.status,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style,
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,18 +2,32 @@
|
||||
import { ParseUptime, GetMinuteStartNowTimestampUTC } from "$lib/server/tool.js";
|
||||
import { makeBadge } from "badge-maker";
|
||||
import moment from "moment";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import {
|
||||
GetMonitors,
|
||||
GetLastStatusBefore,
|
||||
InterpolateData,
|
||||
AggregateData,
|
||||
GetMonitoringData,
|
||||
GetMonitoringDataAll,
|
||||
GetSiteDataByKey,
|
||||
GetLastStatusBeforeAll,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import { ErrorSvg } from "$lib/anywhere.js";
|
||||
|
||||
export async function GET({ params, url }) {
|
||||
// @ts-ignore
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const { name, tag, include_degraded_in_downtime } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
if (monitors.length === 0) {
|
||||
return new Response(ErrorSvg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let name, tag, include_degraded_in_downtime;
|
||||
const siteName = await GetSiteDataByKey("siteName");
|
||||
|
||||
const query = url.searchParams;
|
||||
let sinceLast = query.get("sinceLast");
|
||||
if (sinceLast == undefined || isNaN(sinceLast) || sinceLast < 60) {
|
||||
@@ -22,7 +36,7 @@ export async function GET({ params, url }) {
|
||||
const rangeInSeconds = sinceLast;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const since = GetMinuteStartNowTimestampUTC() - rangeInSeconds;
|
||||
let label = query.get("label") || name;
|
||||
|
||||
const hideDuration = query.get("hideDuration") === "true";
|
||||
|
||||
const duration = moment.duration(rangeInSeconds, "seconds");
|
||||
@@ -35,10 +49,32 @@ export async function GET({ params, url }) {
|
||||
formatted = `${duration.minutes()}m`;
|
||||
}
|
||||
|
||||
let todayDataDb = await db.getMonitoringData(tag, since, now);
|
||||
let anchorStatus = await GetLastStatusBefore(tag, since);
|
||||
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
|
||||
let todayDataDb, anchorStatus;
|
||||
if (params.tag == "_") {
|
||||
name = siteName;
|
||||
let activeTags = monitors.map((monitor) => monitor.tag);
|
||||
//if anyone has include_degraded_in_downtime = "YES" then we will include degraded in downtime
|
||||
let hasIncludeDowntime = monitors.find((monitor) => monitor.include_degraded_in_downtime === "YES");
|
||||
include_degraded_in_downtime = hasIncludeDowntime ? "YES" : "NO";
|
||||
todayDataDb = await GetMonitoringDataAll(activeTags, since, now);
|
||||
anchorStatus = await GetLastStatusBeforeAll(activeTags, since);
|
||||
} else {
|
||||
const m = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
if (!!!m) {
|
||||
return new Response(ErrorSvg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
tag = m.tag;
|
||||
name = m.name;
|
||||
include_degraded_in_downtime = m.include_degraded_in_downtime;
|
||||
|
||||
todayDataDb = await GetMonitoringData(tag, since, now);
|
||||
anchorStatus = await GetLastStatusBefore(tag, since);
|
||||
}
|
||||
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
|
||||
let calculatedData = AggregateData(todayDataDb);
|
||||
|
||||
let numerator = calculatedData.UPs + calculatedData.DEGRADEDs;
|
||||
@@ -48,7 +84,7 @@ export async function GET({ params, url }) {
|
||||
}
|
||||
|
||||
let uptime = ParseUptime(numerator, denominator) + "%";
|
||||
|
||||
let label = query.get("label") || name;
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
const color = query.get("color") || "#0079FF";
|
||||
const style = query.get("style") || "flat";
|
||||
|
||||
@@ -5,5 +5,5 @@ import { base } from "$app/paths";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
throw redirect(302, base + "/incidents/" + format(new Date(), "MMMM-yyyy"));
|
||||
throw redirect(302, base + "/incidents/" + format(new Date(), "MMMM-yyyy"));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import { BeginningOfDay } from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { format, addMonths, parse, subMonths, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
|
||||
import { format, addDays, subDays, parse, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
|
||||
import { f } from "$lib/i18n/client";
|
||||
|
||||
export async function load({ parent, url, params, cookies }) {
|
||||
@@ -16,17 +16,33 @@ export async function load({ parent, url, params, cookies }) {
|
||||
const siteData = parentData.site;
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const query = url.searchParams;
|
||||
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
|
||||
let tomorrowStart = todayStart + 86400;
|
||||
let locale = GetLocaleFromCookie(parentData.site, cookies);
|
||||
let month = params.month; //January-2021
|
||||
|
||||
if (!!!month) {
|
||||
month = format(new Date(), "MMMM-yyyy");
|
||||
}
|
||||
let monthStart = startOfMonth(parse(month, "MMMM-yyyy", new Date()));
|
||||
let monthEnd = endOfMonth(parse(month, "MMMM-yyyy", new Date()));
|
||||
let nextMonth = addDays(monthEnd, 1);
|
||||
let prevMonth = subDays(monthStart, 1);
|
||||
|
||||
let monthStartTs = getUnixTime(startOfMonth(parse(month, "MMMM-yyyy", new Date())));
|
||||
// let xx = parse(month, "MMMM-yyyy", new Date());
|
||||
|
||||
let monthEndTs = getUnixTime(endOfMonth(parse(month, "MMMM-yyyy", new Date())));
|
||||
let midnightMonthStartUTCTimestamp = getUnixTime(monthStart);
|
||||
let midnightMonthEndUTCTimestamp = getUnixTime(monthEnd);
|
||||
let midnightNextMonthUTCTimestamp = getUnixTime(nextMonth);
|
||||
let midnightPrevMonthUTCTimestamp = getUnixTime(prevMonth);
|
||||
|
||||
let monthStartTs = BeginningOfDay({
|
||||
date: new Date(midnightMonthStartUTCTimestamp * 1000),
|
||||
timeZone: parentData.localTz,
|
||||
});
|
||||
|
||||
let monthEndTs = BeginningOfDay({
|
||||
date: new Date(midnightMonthEndUTCTimestamp * 1000),
|
||||
timeZone: parentData.localTz,
|
||||
});
|
||||
|
||||
let incidents = await GetIncidentsPage(monthStartTs, monthEndTs);
|
||||
|
||||
@@ -49,9 +65,12 @@ export async function load({ parent, url, params, cookies }) {
|
||||
|
||||
return {
|
||||
incidents: incidents,
|
||||
nextMonthName: f(addMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
|
||||
prevMonthName: f(subMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
|
||||
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en", parentData.localTz),
|
||||
nextMonthName: f(new Date(midnightNextMonthUTCTimestamp * 1000), "MMMM-yyyy", "en", parentData.localTz),
|
||||
prevMonthName: f(new Date(midnightPrevMonthUTCTimestamp * 1000), "MMMM-yyyy", "en", parentData.localTz),
|
||||
thisMonthName: f(monthEnd, "MMMM-yyyy", "en", parentData.localTz),
|
||||
canonical: siteData.siteURL + "/incidents/" + month,
|
||||
midnightNextMonthUTCTimestamp: midnightNextMonthUTCTimestamp,
|
||||
midnightPrevMonthUTCTimestamp: midnightPrevMonthUTCTimestamp,
|
||||
midnightMonthStartUTCTimestamp: midnightMonthStartUTCTimestamp + 86400,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import Incident from "$lib/components/IncidentNew.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { ArrowRight, ArrowLeft, CalendarCheck2, ChevronLeft } from "lucide-svelte";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import ArrowLeft from "lucide-svelte/icons/arrow-left";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import { base } from "$app/paths";
|
||||
import { goto } from "$app/navigation";
|
||||
import { l, f } from "$lib/i18n/client";
|
||||
@@ -85,7 +87,7 @@
|
||||
<div class="mesh mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
|
||||
<h1 class=" text-5xl font-extrabold leading-tight">
|
||||
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
{f(new Date(data.midnightMonthStartUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
</h1>
|
||||
<p class="mx-auto mt-4 max-w-xl font-medium sm:text-xl">
|
||||
{l(data.lang, "Incident Updates")}
|
||||
@@ -141,7 +143,7 @@
|
||||
}}
|
||||
>
|
||||
<ArrowLeft class="arrow mr-2 h-4 w-4" />
|
||||
{f(parse(data.prevMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
{f(new Date(data.midnightPrevMonthUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -150,7 +152,7 @@
|
||||
window.location.href = `${base}/incidents/${data.nextMonthName}`;
|
||||
}}
|
||||
>
|
||||
{f(parse(data.nextMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
{f(new Date(data.midnightNextMonthUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
|
||||
<ArrowRight class="arrow ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<script>
|
||||
import "../../../../app.postcss";
|
||||
import "../../../../kener.css";
|
||||
import "../../../../manage.css";
|
||||
import { base } from "$app/paths";
|
||||
import { setMode, mode, ModeWatcher } from "mode-watcher";
|
||||
setMode("dark");
|
||||
import "../../../../app.postcss";
|
||||
import "../../../../kener.css";
|
||||
import "../../../../manage.css";
|
||||
import { base } from "$app/paths";
|
||||
import { setMode, mode, ModeWatcher } from "mode-watcher";
|
||||
setMode("dark");
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<svelte:head>
|
||||
<meta name="description" content="Set up Kener" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="icon" href="{base}/logo96.png" />
|
||||
<meta name="description" content="Set up Kener" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="icon" href="{base}/logo96.png" />
|
||||
</svelte:head>
|
||||
|
||||
<main class="setup">
|
||||
<!-- ========== HEADER ========== -->
|
||||
|
||||
<!-- ========== END HEADER ========== -->
|
||||
<slot></slot>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
@@ -1,117 +1,118 @@
|
||||
<script>
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
|
||||
let form = {
|
||||
email: ""
|
||||
};
|
||||
export let data;
|
||||
let isResendSet = data.isResendSet && data.isSecretSet;
|
||||
if (!isResendSet && !data.isSMTPSet) {
|
||||
data.view = "error";
|
||||
data.error = `<p>Environment variables are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars</p>
|
||||
let form = {
|
||||
email: ""
|
||||
};
|
||||
export let data;
|
||||
let isResendSet = data.isResendSet && data.isSecretSet;
|
||||
if (!isResendSet && !data.isSMTPSet) {
|
||||
data.view = "error";
|
||||
data.error = `<p>Environment variables are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars</p>
|
||||
<br/>
|
||||
<p>Either Set RESEND_API_KEY, RESEND_SENDER_EMAIL <br>or<br> Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL</p>
|
||||
<br/>
|
||||
<p>If both are set then SMTP will take priority</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isSiteURLSet === false) {
|
||||
data.view = "error";
|
||||
data.error =
|
||||
"Site URL is not set. Read the documentation to set it. https://kener.ing/docs/site";
|
||||
}
|
||||
if (data.isSiteURLSet === false) {
|
||||
data.view = "error";
|
||||
data.error = "Site URL is not set. Read the documentation to set it. https://kener.ing/docs/site";
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Forgot Password Kener</title>
|
||||
<title>Forgot Password Kener</title>
|
||||
</svelte:head>
|
||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img class="mx-auto h-10 w-auto" src="{base}/logo.png" alt="Your Company" />
|
||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight">Reset Password</h2>
|
||||
<p class="mt-4 text-center">Follow instructions to reset your password</p>
|
||||
</div>
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img class="mx-auto h-10 w-auto" src="{base}/logo.png" alt="Your Company" />
|
||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight">Reset Password</h2>
|
||||
<p class="mt-4 text-center">Follow instructions to reset your password</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
{#if data.view == "error"}
|
||||
<Alert.Root variant="destructive" class="my-4">
|
||||
<Alert.Title>Error</Alert.Title>
|
||||
<Alert.Description>{@html data.error}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{#if data.view == "forgot"}
|
||||
<form class="space-y-6" action="{base}/manage/forgot/submit" method="POST">
|
||||
<div>
|
||||
<label for="email" class="block text-sm/6 font-medium">Email address</label>
|
||||
<div class="mt-2">
|
||||
<Input
|
||||
bind:value={form.email}
|
||||
type="email"
|
||||
name="email"
|
||||
id="form_email"
|
||||
autocomplete="email"
|
||||
placeholder="user@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
{#if data.view == "error"}
|
||||
<Alert.Root variant="destructive" class="my-4">
|
||||
<Alert.Title>Error</Alert.Title>
|
||||
<Alert.Description>{@html data.error}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{#if data.view == "forgot"}
|
||||
<form class="space-y-6" action="{base}/manage/forgot/submit" method="POST">
|
||||
<div>
|
||||
<label for="email" class="block text-sm/6 font-medium">Email address</label>
|
||||
<div class="mt-2">
|
||||
<Input
|
||||
bind:value={form.email}
|
||||
type="email"
|
||||
name="email"
|
||||
id="form_email"
|
||||
autocomplete="email"
|
||||
placeholder="user@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" class="w-full">Reset Password</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.view == "sent"}
|
||||
<Alert.Root variant="success" class="my-4">
|
||||
<Alert.Title>Success</Alert.Title>
|
||||
<Alert.Description
|
||||
>An email has been sent to {data.email} with instructions on how to reset your password.</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<div>
|
||||
<Button type="submit" class="w-full">Reset Password</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.view == "sent"}
|
||||
<Alert.Root variant="success" class="my-4">
|
||||
<Alert.Title>Success</Alert.Title>
|
||||
<Alert.Description class="flex flex-col gap-y-2">
|
||||
<p>An email has been sent to {data.email} with instructions on how to reset your password.</p>
|
||||
<p>
|
||||
If you don't see the email, check other places it might be, like your junk, spam, social, or other folders.
|
||||
</p>
|
||||
<p>
|
||||
Once you have reset your password, you can <a
|
||||
class="font-semibold text-blue-500"
|
||||
href="{base}/manage/signin">login</a
|
||||
>
|
||||
</p>
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
|
||||
{#if data.view == "token"}
|
||||
<form class="space-y-6" action="{base}/manage/forgot/reset" method="POST">
|
||||
<div>
|
||||
<label for="password" class="block text-sm/6 font-medium">New password</label>
|
||||
<div class="mt-2">
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="form_password"
|
||||
autocomplete="new-password"
|
||||
placeholder="********"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="hidden"
|
||||
name="token"
|
||||
id="form_password"
|
||||
bind:value={data.token}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if data.view == "token"}
|
||||
<form class="space-y-6" action="{base}/manage/forgot/reset" method="POST">
|
||||
<div>
|
||||
<label for="password" class="block text-sm/6 font-medium">New password</label>
|
||||
<div class="mt-2">
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="form_password"
|
||||
autocomplete="new-password"
|
||||
placeholder="********"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input type="hidden" name="token" id="form_password" bind:value={data.token} required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" class="w-full">Confirm Password</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.view == "success"}
|
||||
<Alert.Root variant="success" class="my-4">
|
||||
<Alert.Title>Success</Alert.Title>
|
||||
<Alert.Description
|
||||
>Your password has been reset successfully. <a
|
||||
class="font-semibold text-blue-500"
|
||||
href="{base}/manage/signin">Login</a
|
||||
> with you new password</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" class="w-full">Confirm Password</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.view == "success"}
|
||||
<Alert.Root variant="success" class="my-4">
|
||||
<Alert.Title>Success</Alert.Title>
|
||||
<Alert.Description
|
||||
>Your password has been reset successfully. <a class="font-semibold text-blue-500" href="{base}/manage/signin"
|
||||
>Login</a
|
||||
> with you new password</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,88 +2,56 @@
|
||||
import { json, redirect } from "@sveltejs/kit";
|
||||
import { base } from "$app/paths";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { Resend } from "resend";
|
||||
import getSMTPTransport from "$lib/server/notification/smtps.js";
|
||||
import { SendEmailWithTemplate, GetSiteLogoURL } from "$lib/server/controllers/controller.js";
|
||||
import forgotPasswordTemplate from "$lib/server/templates/forgot_pass.html?raw";
|
||||
|
||||
import {
|
||||
HashPassword,
|
||||
GenerateSalt,
|
||||
GenerateToken,
|
||||
VerifyToken,
|
||||
GetSMTPFromENV
|
||||
HashPassword,
|
||||
GenerateSalt,
|
||||
GenerateToken,
|
||||
VerifyToken,
|
||||
GetSMTPFromENV,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function POST({ request, cookies }) {
|
||||
//read form post data email and password
|
||||
const formdata = await request.formData();
|
||||
const email = formdata.get("email");
|
||||
const senderEmail = process.env.RESEND_SENDER_EMAIL;
|
||||
const resendKey = process.env.RESEND_API_KEY;
|
||||
const siteURL = await db.getSiteData("siteURL");
|
||||
//read form post data email and password
|
||||
const formdata = await request.formData();
|
||||
const email = formdata.get("email");
|
||||
const senderEmail = process.env.RESEND_SENDER_EMAIL;
|
||||
const resendKey = process.env.RESEND_API_KEY;
|
||||
const siteURL = await db.getSiteData("siteURL");
|
||||
const siteName = await db.getSiteData("siteName");
|
||||
const logo = await db.getSiteData("logo");
|
||||
|
||||
//check if any entry in user table is already there
|
||||
let userDB = await db.getUserByEmail(email);
|
||||
if (!!!userDB) {
|
||||
let errorMessage = "User does not exist";
|
||||
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
|
||||
}
|
||||
//check if any entry in user table is already there
|
||||
let userDB = await db.getUserByEmail(email);
|
||||
if (!!!userDB) {
|
||||
let errorMessage = "User does not exist";
|
||||
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
|
||||
}
|
||||
|
||||
//generate token
|
||||
let token = await GenerateToken({
|
||||
email: email
|
||||
});
|
||||
//generate token
|
||||
let token = await GenerateToken({
|
||||
email: email,
|
||||
});
|
||||
|
||||
//send email with link to reset password
|
||||
let link = siteURL.value + base + "/manage/forgot?view=token&token=" + token;
|
||||
let subject = "[Reset Password] Kener Reset password request";
|
||||
let message = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Reset Password</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Click on the link below to reset your password:</p>
|
||||
<p><a href="${link}">Reset Password</a></p>
|
||||
<p>You can also copy paste this link ${link}</p>
|
||||
<p></p>This link will expire in 1 hour.</p>
|
||||
<p>If you did not request a password reset, please ignore this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
let emailText = `
|
||||
//send email with link to reset password
|
||||
let link = siteURL.value + base + "/manage/forgot?view=token&token=" + token;
|
||||
let subject = "[Reset Password] Kener Reset password request";
|
||||
|
||||
let emailText = `
|
||||
Click on the link below to reset your password:
|
||||
${link}
|
||||
This link will expire in 1 hour.
|
||||
If you did not request a password reset, please ignore this email.
|
||||
`;
|
||||
let mail = {
|
||||
from: senderEmail,
|
||||
to: [email],
|
||||
subject: subject,
|
||||
text: emailText,
|
||||
html: message
|
||||
};
|
||||
|
||||
let smtpData = GetSMTPFromENV();
|
||||
if (!!smtpData) {
|
||||
const transporter = getSMTPTransport(smtpData);
|
||||
const mailOptions = {
|
||||
from: smtpData.smtp_from_email,
|
||||
to: email,
|
||||
subject: mail.subject,
|
||||
html: mail.html,
|
||||
text: mail.text
|
||||
};
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const resend = new Resend(resendKey);
|
||||
await resend.emails.send(mail);
|
||||
}
|
||||
let emailData = {
|
||||
logo_url: await GetSiteLogoURL(siteURL.value, logo.value, base),
|
||||
link: link,
|
||||
brand_name: siteName.value,
|
||||
};
|
||||
|
||||
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
|
||||
await SendEmailWithTemplate(forgotPasswordTemplate, emailData, email, subject, emailText);
|
||||
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
GetActiveInvitationByToken,
|
||||
UpdateUserData,
|
||||
UpdateInvitationStatusToAccepted,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import { INVITE_VERIFY_EMAIL } from "$lib/server/constants.js";
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
const query = url.searchParams;
|
||||
const invitation_token = query.get("token");
|
||||
let invite = await GetActiveInvitationByToken(invitation_token);
|
||||
let header = "Error";
|
||||
let message = "Invalid Invitation Token. The invitation token is invalid or expired.";
|
||||
let error = true;
|
||||
if (!!invite) {
|
||||
if (invite.invitation_type === INVITE_VERIFY_EMAIL) {
|
||||
let updateData = {
|
||||
userID: invite.invited_user_id,
|
||||
updateKey: "is_verified",
|
||||
updateValue: 1,
|
||||
};
|
||||
try {
|
||||
let updateResp = await UpdateUserData(updateData);
|
||||
let updateInvitationResp = await UpdateInvitationStatusToAccepted(invitation_token);
|
||||
error = false;
|
||||
header = "Email Verified Successfully";
|
||||
message = "Thanks for verifying your email";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
error: error,
|
||||
message: message,
|
||||
header: header,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Accept Invitation from Kener</title>
|
||||
</svelte:head>
|
||||
<div class="mx-auto mt-32 flex max-w-[475px] flex-col gap-y-4 text-center">
|
||||
{#if data.error}
|
||||
<picture class="mx-auto">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a8/512.webp" type="image/webp" />
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a8/512.gif" alt="🚨" width="32" height="32" />
|
||||
</picture>
|
||||
{:else}
|
||||
<picture class="mx-auto">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f38a/512.webp" type="image/webp" />
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f38a/512.gif" alt="🎊" width="32" height="32" />
|
||||
</picture>
|
||||
{/if}
|
||||
<h2 class="text-xl">
|
||||
{data.header}
|
||||
</h2>
|
||||
<p class="text-sm">
|
||||
{data.message}
|
||||
</p>
|
||||
{#if data.error}
|
||||
<Button href="{base}/" class="mt-4 ">Go back</Button>
|
||||
{:else}
|
||||
<Button href="{base}/manage/app/site" class="mt-4 ">Go to Dashboard</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,7 +2,13 @@
|
||||
import { json, redirect } from "@sveltejs/kit";
|
||||
import { base } from "$app/paths";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { HashPassword, GenerateSalt, GenerateToken, CookieConfig } from "$lib/server/controllers/controller.js";
|
||||
import {
|
||||
HashPassword,
|
||||
GenerateSalt,
|
||||
GenerateToken,
|
||||
CookieConfig,
|
||||
ValidatePassword,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
//function to validate a strong password
|
||||
/**
|
||||
@@ -12,16 +18,10 @@ import { HashPassword, GenerateSalt, GenerateToken, CookieConfig } from "$lib/se
|
||||
* - Contains at least one uppercase letter.
|
||||
* - Contains at least one letter (either lowercase or uppercase).
|
||||
* - Has a minimum length of 8 characters.
|
||||
*
|
||||
* @param {string} password - The password to validate.
|
||||
* @returns {boolean} - Returns true if the password meets the criteria, otherwise false.
|
||||
*/
|
||||
function validatePassword(password) {
|
||||
return /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/.test(password);
|
||||
}
|
||||
|
||||
export async function POST({ request, cookies }) {
|
||||
//read form post data email and passowrd
|
||||
//read form post data email and password
|
||||
const formdata = await request.formData();
|
||||
const email = formdata.get("email");
|
||||
const password = formdata.get("password");
|
||||
@@ -34,7 +34,7 @@ export async function POST({ request, cookies }) {
|
||||
throw redirect(302, base + "/manage/setup?error=" + errorMessage);
|
||||
}
|
||||
//validate password
|
||||
if (!validatePassword(password)) {
|
||||
if (!ValidatePassword(password)) {
|
||||
let errorMessage =
|
||||
"Password must contain at least one digit, one lowercase letter, one uppercase letter, and have a minimum length of 8 characters.";
|
||||
throw redirect(302, base + "/manage/setup?error=" + errorMessage);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
GetAllSiteData,
|
||||
VerifyToken,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import { GetAllSiteData, VerifyToken, IsLoggedInSession } from "$lib/server/controllers/controller.js";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { base } from "$app/paths";
|
||||
import { MaskString } from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import version from "$lib/version.js";
|
||||
//write a function to mask a string, just have last 4 characters visible
|
||||
|
||||
export async function load({ params, route, url, cookies, request }) {
|
||||
@@ -15,29 +13,19 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
if (process.env.KENER_SECRET_KEY === undefined) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
let tokenData = cookies.get("kener-user");
|
||||
|
||||
if (!!!tokenData) {
|
||||
//redirect to signin page if user is not authenticated
|
||||
throw redirect(302, base + "/manage/signin");
|
||||
let userDB = null;
|
||||
let isLoggedIn = await IsLoggedInSession(cookies);
|
||||
if (!!isLoggedIn.error) {
|
||||
throw redirect(302, base + isLoggedIn.location);
|
||||
}
|
||||
|
||||
//get user by email
|
||||
let tokenUser = await VerifyToken(tokenData);
|
||||
if (!!!tokenUser) {
|
||||
//redirect to signin page if user is not authenticated
|
||||
throw redirect(302, base + "/manage/signin/logout");
|
||||
}
|
||||
let userDB = await db.getUserByEmail(tokenUser.email);
|
||||
if (!!!userDB) {
|
||||
//redirect to signin page if user is not authenticated
|
||||
throw redirect(302, base + "/manage/signin");
|
||||
}
|
||||
userDB = isLoggedIn.user;
|
||||
|
||||
return {
|
||||
siteData,
|
||||
KENER_SECRET_KEY: !!process.env.KENER_SECRET_KEY
|
||||
? MaskString(process.env.KENER_SECRET_KEY)
|
||||
: "",
|
||||
KENER_SECRET_KEY: !!process.env.KENER_SECRET_KEY ? MaskString(process.env.KENER_SECRET_KEY) : "",
|
||||
user: userDB,
|
||||
kenerVersion: version(),
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user