Compare commits
21 Commits
custom-dep
...
AUT-980
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d893a36a47 | ||
![]() |
c62aea3852 | ||
![]() |
58c90da385 | ||
![]() |
c095d3138b | ||
![]() |
9548c93b4c | ||
![]() |
4144944ab2 | ||
![]() |
46b85519c1 | ||
![]() |
5a83fc33ec | ||
![]() |
c80791267f | ||
![]() |
b30f97db3e | ||
![]() |
717c81fa2b | ||
![]() |
ae188bc563 | ||
![]() |
fc4561221d | ||
![]() |
5aeb4f8809 | ||
![]() |
c6c900bc39 | ||
![]() |
c18ab67a25 | ||
![]() |
55ae1470d0 | ||
![]() |
a1136fdfb2 | ||
![]() |
3da5e13ecd | ||
![]() |
40d0fe0db6 | ||
![]() |
029fd2d0b0 |
@@ -36,7 +36,6 @@ services:
|
|||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:21.1
|
image: quay.io/keycloak/keycloak:21.1
|
||||||
restart: always
|
restart: always
|
||||||
container_name: keycloak
|
|
||||||
environment:
|
environment:
|
||||||
- KEYCLOAK_ADMIN=admin
|
- KEYCLOAK_ADMIN=admin
|
||||||
- KEYCLOAK_ADMIN_PASSWORD=admin
|
- KEYCLOAK_ADMIN_PASSWORD=admin
|
||||||
|
@@ -2,6 +2,7 @@ import appConfig from '../../src/config/app.js';
|
|||||||
import logger from '../../src/helpers/logger.js';
|
import logger from '../../src/helpers/logger.js';
|
||||||
import client from './client.js';
|
import client from './client.js';
|
||||||
import User from '../../src/models/user.js';
|
import User from '../../src/models/user.js';
|
||||||
|
import Config from '../../src/models/config.js';
|
||||||
import Role from '../../src/models/role.js';
|
import Role from '../../src/models/role.js';
|
||||||
import '../../src/config/orm.js';
|
import '../../src/config/orm.js';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
@@ -21,6 +22,14 @@ export async function createUser(
|
|||||||
email = 'user@automatisch.io',
|
email = 'user@automatisch.io',
|
||||||
password = 'sample'
|
password = 'sample'
|
||||||
) {
|
) {
|
||||||
|
if (appConfig.disableSeedUser) {
|
||||||
|
logger.info('Seed user is disabled.');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const UNIQUE_VIOLATION_CODE = '23505';
|
const UNIQUE_VIOLATION_CODE = '23505';
|
||||||
|
|
||||||
const role = await fetchAdminRole();
|
const role = await fetchAdminRole();
|
||||||
@@ -37,6 +46,8 @@ export async function createUser(
|
|||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
const user = await User.query().insertAndFetch(userParams);
|
const user = await User.query().insertAndFetch(userParams);
|
||||||
logger.info(`User has been saved: ${user.email}`);
|
logger.info(`User has been saved: ${user.email}`);
|
||||||
|
|
||||||
|
await Config.markInstallationCompleted();
|
||||||
} else {
|
} else {
|
||||||
logger.info('No need to seed a user.');
|
logger.info('No need to seed a user.');
|
||||||
}
|
}
|
||||||
|
@@ -67,6 +67,7 @@
|
|||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.7.1",
|
"winston": "^3.7.1",
|
||||||
"xmlrpc": "^1.3.2"
|
"xmlrpc": "^1.3.2"
|
||||||
},
|
},
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Create a watch',
|
||||||
|
key: 'createWatch',
|
||||||
|
description: 'Creates a new change detection watch for a specific website.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'URL',
|
||||||
|
key: 'url',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
description: 'Url you want to monitor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const url = $.step.parameters.url;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await $.http.post('/v1/watch', body);
|
||||||
|
|
||||||
|
$.setActionItem({ raw: response.data });
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,29 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Delete a watch',
|
||||||
|
key: 'deleteWatch',
|
||||||
|
description: 'Deletes a change detection watch.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Watch ID',
|
||||||
|
key: 'watchId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
description: 'Watch id you want to delete',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const watchId = $.step.parameters.watchId;
|
||||||
|
|
||||||
|
await $.http.delete(`/v1/watch/${watchId}`);
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
result: 'successful',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,4 @@
|
|||||||
|
import createWatch from './create-watch/index.js';
|
||||||
|
import deleteWatch from './delete-watch/index.js';
|
||||||
|
|
||||||
|
export default [createWatch, deleteWatch];
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.9 KiB |
44
packages/backend/src/apps/changedetection/auth/index.js
Normal file
44
packages/backend/src/apps/changedetection/auth/index.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import verifyCredentials from './verify-credentials.js';
|
||||||
|
import isStillVerified from './is-still-verified.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'screenName',
|
||||||
|
label: 'Screen Name',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'Screen name of your connection to be used on Automatisch UI.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'instanceUrl',
|
||||||
|
label: 'Instance URL',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'apiKey',
|
||||||
|
label: 'API Key',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'Changedetection API key of your account.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
};
|
@@ -0,0 +1,8 @@
|
|||||||
|
import verifyCredentials from './verify-credentials.js';
|
||||||
|
|
||||||
|
const isStillVerified = async ($) => {
|
||||||
|
await verifyCredentials($);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
@@ -0,0 +1,10 @@
|
|||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
await $.http.get('/v1/systeminfo');
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: $.auth.data.screenName,
|
||||||
|
apiKey: $.auth.data.apiKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
@@ -0,0 +1,9 @@
|
|||||||
|
const addAuthHeader = ($, requestConfig) => {
|
||||||
|
if ($.auth.data?.apiKey) {
|
||||||
|
requestConfig.headers['x-api-key'] = $.auth.data.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
@@ -0,0 +1,10 @@
|
|||||||
|
const setBaseUrl = ($, requestConfig) => {
|
||||||
|
const instanceUrl = $.auth.data.instanceUrl;
|
||||||
|
if (instanceUrl) {
|
||||||
|
requestConfig.baseURL = `${instanceUrl}/api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setBaseUrl;
|
@@ -0,0 +1,3 @@
|
|||||||
|
import listWatches from './list-watches/index.js';
|
||||||
|
|
||||||
|
export default [listWatches];
|
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List watches',
|
||||||
|
key: 'listWatches',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const watches = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await $.http.get('/v1/watch');
|
||||||
|
const watchIds = Object.keys(data);
|
||||||
|
|
||||||
|
if (watchIds?.length) {
|
||||||
|
for (const watchId of watchIds) {
|
||||||
|
watches.data.push({
|
||||||
|
value: watchId,
|
||||||
|
name: data[watchId].url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return watches;
|
||||||
|
},
|
||||||
|
};
|
23
packages/backend/src/apps/changedetection/index.js
Normal file
23
packages/backend/src/apps/changedetection/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import addAuthHeader from './common/add-auth-header.js';
|
||||||
|
import auth from './auth/index.js';
|
||||||
|
import setBaseUrl from './common/set-base-url.js';
|
||||||
|
import actions from './actions/index.js';
|
||||||
|
import dynamicData from './dynamic-data/index.js';
|
||||||
|
import triggers from './triggers/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Changedetection',
|
||||||
|
key: 'changedetection',
|
||||||
|
iconUrl: '{BASE_URL}/apps/changedetection/assets/favicon.svg',
|
||||||
|
authDocUrl: '{DOCS_URL}/apps/changedetection/connection',
|
||||||
|
supportsConnections: true,
|
||||||
|
baseUrl: 'https://changedetection.io',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: '3056d3',
|
||||||
|
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||||
|
auth,
|
||||||
|
actions,
|
||||||
|
dynamicData,
|
||||||
|
triggers,
|
||||||
|
});
|
@@ -0,0 +1,43 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'Changed watch',
|
||||||
|
key: 'changedWatch',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when any changes detected.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Watch',
|
||||||
|
key: 'watchId',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listWatches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const watchId = $.step.parameters.watchId;
|
||||||
|
|
||||||
|
const { data } = await $.http.get(`v1/watch/${watchId}`);
|
||||||
|
|
||||||
|
if (Object.keys(data).length !== 0) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: data,
|
||||||
|
meta: {
|
||||||
|
internalId: `${watchId}-${data.last_changed}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,3 @@
|
|||||||
|
import changedWatch from './changed-watch/index.js';
|
||||||
|
|
||||||
|
export default [changedWatch];
|
@@ -98,6 +98,7 @@ const appConfig = {
|
|||||||
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
||||||
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
||||||
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
||||||
|
disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!appConfig.encryptionKey) {
|
if (!appConfig.encryptionKey) {
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import app from '../../../../../app';
|
import app from '../../../../../app';
|
||||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id';
|
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id';
|
||||||
import { createRole } from '../../../../../../test/factories/role';
|
import { createRole } from '../../../../../../test/factories/role';
|
||||||
import { createUser } from '../../../../../../test/factories/user';
|
import { createUser } from '../../../../../../test/factories/user';
|
||||||
import getUsersMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-users.js';
|
import getUsersMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-users.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
|
||||||
|
|
||||||
describe('GET /api/v1/admin/users', () => {
|
describe('GET /api/v1/admin/users', () => {
|
||||||
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
||||||
@@ -32,8 +31,6 @@ describe('GET /api/v1/admin/users', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return users data', async () => {
|
it('should return users data', async () => {
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/api/v1/admin/users')
|
.get('/api/v1/admin/users')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
import User from '../../../../../models/user.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const { email, password, fullName } = request.body;
|
||||||
|
|
||||||
|
await User.createAdmin({ email, password, fullName });
|
||||||
|
|
||||||
|
response.status(204).end();
|
||||||
|
};
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../../../../app.js';
|
||||||
|
import Config from '../../../../../models/config.js';
|
||||||
|
import User from '../../../../../models/user.js';
|
||||||
|
import { createRole } from '../../../../../../test/factories/role';
|
||||||
|
import { createUser } from '../../../../../../test/factories/user';
|
||||||
|
import { createInstallationCompletedConfig } from '../../../../../../test/factories/config';
|
||||||
|
|
||||||
|
describe('POST /api/v1/installation/users', () => {
|
||||||
|
let adminRole;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
adminRole = await createRole({
|
||||||
|
name: 'Admin',
|
||||||
|
key: 'admin',
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for incomplete installations', () => {
|
||||||
|
it('should respond with HTTP 204 with correct payload when no user', async () => {
|
||||||
|
expect(await Config.isInstallationCompleted()).toBe(false);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/v1/installation/users')
|
||||||
|
.send({
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'password',
|
||||||
|
fullName: 'Initial admin'
|
||||||
|
})
|
||||||
|
.expect(204);
|
||||||
|
|
||||||
|
const user = await User.query().findOne({ email: 'user@automatisch.io' });
|
||||||
|
|
||||||
|
expect(user.roleId).toBe(adminRole.id);
|
||||||
|
expect(await Config.isInstallationCompleted()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with HTTP 403 with correct payload when one user exists at least', async () => {
|
||||||
|
expect(await Config.isInstallationCompleted()).toBe(false);
|
||||||
|
|
||||||
|
await createUser();
|
||||||
|
|
||||||
|
const usersCountBefore = await User.query().resultSize();
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/v1/installation/users')
|
||||||
|
.send({
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'password',
|
||||||
|
fullName: 'Initial admin'
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
const usersCountAfter = await User.query().resultSize();
|
||||||
|
|
||||||
|
expect(usersCountBefore).toEqual(usersCountAfter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for completed installations', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createInstallationCompletedConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with HTTP 403 when installation completed', async () => {
|
||||||
|
expect(await Config.isInstallationCompleted()).toBe(true);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/v1/installation/users')
|
||||||
|
.send({
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'password',
|
||||||
|
fullName: 'Initial admin'
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
const user = await User.query().findOne({ email: 'user@automatisch.io' });
|
||||||
|
|
||||||
|
expect(user).toBeUndefined();
|
||||||
|
expect(await Config.isInstallationCompleted()).toBe(true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,11 @@
|
|||||||
|
export async function up(knex) {
|
||||||
|
return knex.schema.table('access_tokens', (table) => {
|
||||||
|
table.string('saml_session_id').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
return knex.schema.table('access_tokens', (table) => {
|
||||||
|
table.dropColumn('saml_session_id');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
export async function up(knex) {
|
||||||
|
const users = await knex('users').limit(1);
|
||||||
|
|
||||||
|
// no user implies installation is not completed yet.
|
||||||
|
if (users.length === 0) return;
|
||||||
|
|
||||||
|
await knex('config').insert({
|
||||||
|
key: 'installation.completed',
|
||||||
|
value: {
|
||||||
|
data: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
await knex('config').where({ key: 'installation.completed' }).delete();
|
||||||
|
};
|
16
packages/backend/src/helpers/allow-installation.js
Normal file
16
packages/backend/src/helpers/allow-installation.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Config from '../models/config.js';
|
||||||
|
import User from '../models/user.js';
|
||||||
|
|
||||||
|
export async function allowInstallation(request, response, next) {
|
||||||
|
if (await Config.isInstallationCompleted()) {
|
||||||
|
return response.status(403).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAnyUsers = await User.query().resultSize() > 0;
|
||||||
|
|
||||||
|
if (hasAnyUsers) {
|
||||||
|
return response.status(403).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
@@ -4,12 +4,13 @@ import AccessToken from '../models/access-token.js';
|
|||||||
|
|
||||||
const TOKEN_EXPIRES_IN = 14 * 24 * 60 * 60; // 14 days in seconds
|
const TOKEN_EXPIRES_IN = 14 * 24 * 60 * 60; // 14 days in seconds
|
||||||
|
|
||||||
const createAuthTokenByUserId = async (userId) => {
|
const createAuthTokenByUserId = async (userId, samlSessionId) => {
|
||||||
const user = await User.query().findById(userId).throwIfNotFound();
|
const user = await User.query().findById(userId).throwIfNotFound();
|
||||||
const token = await crypto.randomBytes(48).toString('hex');
|
const token = await crypto.randomBytes(48).toString('hex');
|
||||||
|
|
||||||
await AccessToken.query().insert({
|
await AccessToken.query().insert({
|
||||||
token,
|
token,
|
||||||
|
samlSessionId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
expiresIn: TOKEN_EXPIRES_IN,
|
expiresIn: TOKEN_EXPIRES_IN,
|
||||||
});
|
});
|
||||||
|
@@ -5,8 +5,11 @@ import passport from 'passport';
|
|||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import createAuthTokenByUserId from './create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from './create-auth-token-by-user-id.js';
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee.js';
|
import SamlAuthProvider from '../models/saml-auth-provider.ee.js';
|
||||||
|
import AccessToken from '../models/access-token.js';
|
||||||
import findOrCreateUserBySamlIdentity from './find-or-create-user-by-saml-identity.ee.js';
|
import findOrCreateUserBySamlIdentity from './find-or-create-user-by-saml-identity.ee.js';
|
||||||
|
|
||||||
|
const asyncNoop = async () => { };
|
||||||
|
|
||||||
export default function configurePassport(app) {
|
export default function configurePassport(app) {
|
||||||
app.use(
|
app.use(
|
||||||
passport.initialize({
|
passport.initialize({
|
||||||
@@ -19,6 +22,10 @@ export default function configurePassport(app) {
|
|||||||
{
|
{
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
getSamlOptions: async function (request, done) {
|
getSamlOptions: async function (request, done) {
|
||||||
|
// This is a workaround to avoid session logout which passport-saml enforces
|
||||||
|
request.logout = asyncNoop;
|
||||||
|
request.logOut = asyncNoop;
|
||||||
|
|
||||||
const { issuer } = request.params;
|
const { issuer } = request.params;
|
||||||
const notFoundIssuer = new Error('Issuer cannot be found!');
|
const notFoundIssuer = new Error('Issuer cannot be found!');
|
||||||
|
|
||||||
@@ -35,7 +42,7 @@ export default function configurePassport(app) {
|
|||||||
return done(null, authProvider.config);
|
return done(null, authProvider.config);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async function (request, user, done) {
|
async function signonVerify(request, user, done) {
|
||||||
const { issuer } = request.params;
|
const { issuer } = request.params;
|
||||||
const notFoundIssuer = new Error('Issuer cannot be found!');
|
const notFoundIssuer = new Error('Issuer cannot be found!');
|
||||||
|
|
||||||
@@ -53,10 +60,38 @@ export default function configurePassport(app) {
|
|||||||
user,
|
user,
|
||||||
authProvider
|
authProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
|
request.samlSessionId = user.sessionIndex;
|
||||||
|
|
||||||
return done(null, foundUserWithIdentity);
|
return done(null, foundUserWithIdentity);
|
||||||
},
|
},
|
||||||
function (request, user, done) {
|
async function logoutVerify(request, user, done) {
|
||||||
return done(null, null);
|
const { issuer } = request.params;
|
||||||
|
const notFoundIssuer = new Error('Issuer cannot be found!');
|
||||||
|
|
||||||
|
if (!issuer) return done(notFoundIssuer);
|
||||||
|
|
||||||
|
const authProvider = await SamlAuthProvider.query().findOne({
|
||||||
|
issuer: request.params.issuer,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authProvider) {
|
||||||
|
return done(notFoundIssuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundUserWithIdentity = await findOrCreateUserBySamlIdentity(
|
||||||
|
user,
|
||||||
|
authProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
const accessToken = await AccessToken.query().findOne({
|
||||||
|
revoked_at: null,
|
||||||
|
saml_session_id: user.sessionIndex,
|
||||||
|
}).throwIfNotFound();
|
||||||
|
|
||||||
|
await accessToken.revoke();
|
||||||
|
|
||||||
|
return done(null, foundUserWithIdentity);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -73,17 +108,22 @@ export default function configurePassport(app) {
|
|||||||
'/login/saml/:issuer/callback',
|
'/login/saml/:issuer/callback',
|
||||||
passport.authenticate('saml', {
|
passport.authenticate('saml', {
|
||||||
session: false,
|
session: false,
|
||||||
failureRedirect: '/',
|
|
||||||
failureFlash: true,
|
|
||||||
}),
|
}),
|
||||||
async (req, res) => {
|
async (request, response) => {
|
||||||
const token = await createAuthTokenByUserId(req.currentUser.id);
|
const token = await createAuthTokenByUserId(request.currentUser.id, request.samlSessionId);
|
||||||
|
|
||||||
const redirectUrl = new URL(
|
const redirectUrl = new URL(
|
||||||
`/login/callback?token=${token}`,
|
`/login/callback?token=${token}`,
|
||||||
appConfig.webAppUrl
|
appConfig.webAppUrl
|
||||||
).toString();
|
).toString();
|
||||||
res.redirect(redirectUrl);
|
response.redirect(redirectUrl);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/logout/saml/:issuer',
|
||||||
|
passport.authenticate('saml', {
|
||||||
|
session: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ class AccessToken extends Base {
|
|||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
token: { type: 'string', minLength: 32 },
|
token: { type: 'string', minLength: 32 },
|
||||||
|
samlSessionId: { type: ['string', 'null'] },
|
||||||
expiresIn: { type: 'integer' },
|
expiresIn: { type: 'integer' },
|
||||||
revokedAt: { type: ['string', 'null'], format: 'date-time' },
|
revokedAt: { type: ['string', 'null'], format: 'date-time' },
|
||||||
},
|
},
|
||||||
@@ -28,8 +29,37 @@ class AccessToken extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async terminateRemoteSamlSession() {
|
||||||
|
if (!this.samlSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this
|
||||||
|
.$relatedQuery('user');
|
||||||
|
|
||||||
|
const firstIdentity = await user
|
||||||
|
.$relatedQuery('identities')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const samlAuthProvider = await firstIdentity
|
||||||
|
.$relatedQuery('samlAuthProvider')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const response = await samlAuthProvider.terminateRemoteSession(this.samlSessionId);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
async revoke() {
|
async revoke() {
|
||||||
return await this.$query().patch({ revokedAt: new Date().toISOString() });
|
const response = await this.$query().patch({ revokedAt: new Date().toISOString() });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.terminateRemoteSamlSession();
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: should it silently fail or not?
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,6 +13,28 @@ class Config extends Base {
|
|||||||
value: { type: 'object' },
|
value: { type: 'object' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static async isInstallationCompleted() {
|
||||||
|
const installationCompletedEntry = await this
|
||||||
|
.query()
|
||||||
|
.where({
|
||||||
|
key: 'installation.completed'
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const installationCompleted = installationCompletedEntry?.value?.data === true;
|
||||||
|
|
||||||
|
return installationCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markInstallationCompleted() {
|
||||||
|
return await this.query().insert({
|
||||||
|
key: 'installation.completed',
|
||||||
|
value: {
|
||||||
|
data: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
@@ -45,6 +45,10 @@ class Role extends Base {
|
|||||||
get isAdmin() {
|
get isAdmin() {
|
||||||
return this.key === 'admin';
|
return this.key === 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async findAdmin() {
|
||||||
|
return await this.query().findOne({ key: 'admin' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Role;
|
export default Role;
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
||||||
@@ -61,27 +63,71 @@ class SamlAuthProvider extends Base {
|
|||||||
});
|
});
|
||||||
|
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['loginUrl'];
|
return ['loginUrl', 'remoteLogoutUrl'];
|
||||||
}
|
}
|
||||||
|
|
||||||
get loginUrl() {
|
get loginUrl() {
|
||||||
return new URL(`/login/saml/${this.issuer}`, appConfig.baseUrl).toString();
|
return new URL(`/login/saml/${this.issuer}`, appConfig.baseUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get loginCallBackUrl() {
|
||||||
const callbackUrl = new URL(
|
return new URL(
|
||||||
`/login/saml/${this.issuer}/callback`,
|
`/login/saml/${this.issuer}/callback`,
|
||||||
appConfig.baseUrl
|
appConfig.baseUrl
|
||||||
).toString();
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get remoteLogoutUrl() {
|
||||||
|
return this.entryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
return {
|
return {
|
||||||
callbackUrl,
|
callbackUrl: this.loginCallBackUrl,
|
||||||
cert: this.certificate,
|
cert: this.certificate,
|
||||||
entryPoint: this.entryPoint,
|
entryPoint: this.entryPoint,
|
||||||
issuer: this.issuer,
|
issuer: this.issuer,
|
||||||
signatureAlgorithm: this.signatureAlgorithm,
|
signatureAlgorithm: this.signatureAlgorithm,
|
||||||
|
logoutUrl: this.remoteLogoutUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateLogoutRequestBody(sessionId) {
|
||||||
|
const logoutRequest = `
|
||||||
|
<samlp:LogoutRequest
|
||||||
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||||
|
ID="${uuidv4()}"
|
||||||
|
Version="2.0"
|
||||||
|
IssueInstant="${new Date().toISOString()}"
|
||||||
|
Destination="${this.remoteLogoutUrl}">
|
||||||
|
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${this.issuer}</saml:Issuer>
|
||||||
|
<samlp:SessionIndex>${sessionId}</samlp:SessionIndex>
|
||||||
|
</samlp:LogoutRequest>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const encodedLogoutRequest = Buffer.from(logoutRequest).toString('base64')
|
||||||
|
|
||||||
|
return encodedLogoutRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminateRemoteSession(sessionId) {
|
||||||
|
const logoutRequest = this.generateLogoutRequestBody(sessionId);
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
this.remoteLogoutUrl,
|
||||||
|
new URLSearchParams({
|
||||||
|
SAMLRequest: logoutRequest,
|
||||||
|
}).toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SamlAuthProvider;
|
export default SamlAuthProvider;
|
||||||
|
@@ -10,6 +10,7 @@ import Base from './base.js';
|
|||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AccessToken from './access-token.js';
|
import AccessToken from './access-token.js';
|
||||||
import Connection from './connection.js';
|
import Connection from './connection.js';
|
||||||
|
import Config from './config.js';
|
||||||
import Execution from './execution.js';
|
import Execution from './execution.js';
|
||||||
import Flow from './flow.js';
|
import Flow from './flow.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
@@ -373,6 +374,21 @@ class User extends Base {
|
|||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async createAdmin({ email, password, fullName }) {
|
||||||
|
const adminRole = await Role.findAdmin();
|
||||||
|
|
||||||
|
const adminUser = await this.query().insert({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
fullName,
|
||||||
|
roleId: adminRole.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await Config.markInstallationCompleted();
|
||||||
|
|
||||||
|
return adminUser;
|
||||||
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
@@ -2,25 +2,17 @@ import { Router } from 'express';
|
|||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { authenticateUser } from '../../../../helpers/authentication.js';
|
import { authenticateUser } from '../../../../helpers/authentication.js';
|
||||||
import { authorizeAdmin } from '../../../../helpers/authorization.js';
|
import { authorizeAdmin } from '../../../../helpers/authorization.js';
|
||||||
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
|
|
||||||
import getUsersAction from '../../../../controllers/api/v1/admin/users/get-users.ee.js';
|
import getUsersAction from '../../../../controllers/api/v1/admin/users/get-users.ee.js';
|
||||||
import getUserAction from '../../../../controllers/api/v1/admin/users/get-user.ee.js';
|
import getUserAction from '../../../../controllers/api/v1/admin/users/get-user.ee.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get(
|
router.get('/', authenticateUser, authorizeAdmin, asyncHandler(getUsersAction));
|
||||||
'/',
|
|
||||||
authenticateUser,
|
|
||||||
authorizeAdmin,
|
|
||||||
checkIsEnterprise,
|
|
||||||
asyncHandler(getUsersAction)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
authorizeAdmin,
|
authorizeAdmin,
|
||||||
checkIsEnterprise,
|
|
||||||
asyncHandler(getUserAction)
|
asyncHandler(getUserAction)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
14
packages/backend/src/routes/api/v1/installation/users.js
Normal file
14
packages/backend/src/routes/api/v1/installation/users.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import asyncHandler from 'express-async-handler';
|
||||||
|
import { allowInstallation } from '../../../../helpers/allow-installation.js';
|
||||||
|
import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
allowInstallation,
|
||||||
|
asyncHandler(createUserAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@@ -18,6 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.
|
|||||||
import rolesRouter from './api/v1/admin/roles.ee.js';
|
import rolesRouter from './api/v1/admin/roles.ee.js';
|
||||||
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
||||||
import adminUsersRouter from './api/v1/admin/users.ee.js';
|
import adminUsersRouter from './api/v1/admin/users.ee.js';
|
||||||
|
import installationUsersRouter from './api/v1/installation/users.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -40,5 +41,7 @@ router.use('/api/v1/admin/users', adminUsersRouter);
|
|||||||
router.use('/api/v1/admin/roles', rolesRouter);
|
router.use('/api/v1/admin/roles', rolesRouter);
|
||||||
router.use('/api/v1/admin/permissions', permissionsRouter);
|
router.use('/api/v1/admin/permissions', permissionsRouter);
|
||||||
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
|
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
|
||||||
|
router.use('/api/v1/installation/users', installationUsersRouter);
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@@ -11,3 +11,7 @@ export const createConfig = async (params = {}) => {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createInstallationCompletedConfig = async () => {
|
||||||
|
return await createConfig({ key: 'installation.completed', value: { data: true } });
|
||||||
|
}
|
||||||
|
@@ -8,7 +8,7 @@ global.beforeAll(async () => {
|
|||||||
logger.silent = true;
|
logger.silent = true;
|
||||||
|
|
||||||
// Remove default roles and permissions before running the test suite
|
// Remove default roles and permissions before running the test suite
|
||||||
await knex.raw('TRUNCATE TABLE roles, permissions CASCADE');
|
await knex.raw('TRUNCATE TABLE config, roles, permissions CASCADE');
|
||||||
});
|
});
|
||||||
|
|
||||||
global.beforeEach(async () => {
|
global.beforeEach(async () => {
|
||||||
|
@@ -41,6 +41,16 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/carbone/connection' },
|
{ text: 'Connection', link: '/apps/carbone/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Changedetection',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/changedetection/triggers' },
|
||||||
|
{ text: 'Actions', link: '/apps/changedetection/actions' },
|
||||||
|
{ text: 'Connection', link: '/apps/changedetection/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Datastore',
|
text: 'Datastore',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
14
packages/docs/pages/apps/changedetection/actions.md
Normal file
14
packages/docs/pages/apps/changedetection/actions.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/changedetection.svg
|
||||||
|
items:
|
||||||
|
- name: Create a watch
|
||||||
|
desc: Creates a new change detection watch for a specific website.
|
||||||
|
- name: Delete a watch
|
||||||
|
desc: Deletes a change detection watch.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
14
packages/docs/pages/apps/changedetection/connection.md
Normal file
14
packages/docs/pages/apps/changedetection/connection.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Changedetection
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Changedetection
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Go to your Changedetection admin panel.
|
||||||
|
2. Click on the **Settings** button.
|
||||||
|
3. Click on the **API** tab.
|
||||||
|
4. Copy **API key** from the page to the `API Key` field on Automatisch.
|
||||||
|
5. Add your Instance URL in the **Instance URL** field on Automatisch.
|
||||||
|
6. Write any screen name to be displayed in Automatisch.
|
||||||
|
7. Now, you can start using the Changedetection connection with Automatisch.
|
12
packages/docs/pages/apps/changedetection/triggers.md
Normal file
12
packages/docs/pages/apps/changedetection/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/changedetection.svg
|
||||||
|
items:
|
||||||
|
- name: Changed watch
|
||||||
|
desc: Triggers when any changes detected.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
@@ -3,6 +3,7 @@
|
|||||||
The following integrations are currently supported by Automatisch.
|
The following integrations are currently supported by Automatisch.
|
||||||
|
|
||||||
- [Carbone](/apps/carbone/actions)
|
- [Carbone](/apps/carbone/actions)
|
||||||
|
- [Changedetection](/apps/changedetection/actions)
|
||||||
- [Datastore](/apps/datastore/actions)
|
- [Datastore](/apps/datastore/actions)
|
||||||
- [DeepL](/apps/deepl/actions)
|
- [DeepL](/apps/deepl/actions)
|
||||||
- [Delay](/apps/delay/actions)
|
- [Delay](/apps/delay/actions)
|
||||||
|
1
packages/docs/pages/public/favicons/changedetection.svg
Normal file
1
packages/docs/pages/public/favicons/changedetection.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.9 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.9 KiB |
@@ -16024,6 +16024,11 @@ uuid@^9.0.0:
|
|||||||
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz"
|
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz"
|
||||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||||
|
|
||||||
|
uuid@^9.0.1:
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||||
|
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3:
|
v8-compile-cache@^2.0.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
|
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
|
||||||
|
Reference in New Issue
Block a user