Compare commits

...

18 Commits

Author SHA1 Message Date
Rıdvan Akca
c095d3138b feat(changedetection): add changedetection integration 2024-05-13 16:28:05 +02:00
Ali BARIN
9548c93b4c Merge pull request #1867 from automatisch/custom-user-seed
add POST /api/v1/installation/users to seed user
2024-05-13 16:19:37 +02:00
Ali BARIN
4144944ab2 refactor(migrations): rename installation completed migration 2024-05-13 14:12:46 +00:00
Ali BARIN
46b85519c1 refactor(User/createAdmin): mark installation completed 2024-05-13 14:10:21 +00:00
Ali BARIN
5a83fc33ec refactor(User): rename createAdminUser with createAdmin 2024-05-13 14:09:09 +00:00
Ali BARIN
c80791267f refactor(installation): improve allow installation guard 2024-05-13 14:00:12 +00:00
Ali BARIN
b30f97db3e feat: add POST /api/v1/installation/users to seed user 2024-05-13 13:31:16 +00:00
Ali BARIN
717c81fa2b test(global-hooks): truncate config table 2024-05-13 13:31:16 +00:00
Ali BARIN
ae188bc563 feat: add migration to mark userful instances installation completed 2024-05-13 13:31:16 +00:00
Ali BARIN
fc4561221d feat: add DISABLE_SEED_USER to bypass yarn db:seed:user command 2024-05-13 13:31:16 +00:00
Ali BARIN
5aeb4f8809 Merge pull request #1883 from automatisch/checkisenterprise
feat: remove checkIsEnterprise middleware from admin users
2024-05-13 15:30:55 +02:00
Ali BARIN
c6c900bc39 test(get-users.ee): remove license mock 2024-05-13 13:20:48 +00:00
Rıdvan Akca
c18ab67a25 feat: remove checkIsEnterprise middleware from admin users 2024-05-13 15:02:10 +02:00
Ali BARIN
55ae1470d0 Merge pull request #1875 from automatisch/logout-saml 2024-05-13 13:51:49 +02:00
Ömer Faruk Aydın
a1136fdfb2 Merge pull request #1882 from automatisch/no-proxy
feat: add no_proxy support in http(s) agents
2024-05-13 13:43:39 +02:00
Ali BARIN
3da5e13ecd feat: support bi-directional backchannel SAML SLO 2024-05-10 08:58:32 +00:00
Ali BARIN
40d0fe0db6 chore: add saml_session_id property in access token 2024-05-10 08:54:02 +00:00
Ali BARIN
029fd2d0b0 chore(devcontainer): remove keycloak custom container name 2024-05-10 08:54:02 +00:00
33 changed files with 474 additions and 29 deletions

View File

@@ -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

View File

@@ -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.');
} }

View File

@@ -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"
}, },

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

View 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,
};

View File

@@ -0,0 +1,8 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
const addAuthHeader = ($, requestConfig) => {
if ($.auth.data?.apiKey) {
requestConfig.headers['x-api-key'] = $.auth.data.apiKey;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,10 @@
const setBaseUrl = ($, requestConfig) => {
const instanceUrl = $.auth.data.instanceUrl;
if (instanceUrl) {
requestConfig.baseURL = `${instanceUrl}/api`;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -0,0 +1,17 @@
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';
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,
});

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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();
};

View File

@@ -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);
});
})
});

View File

@@ -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');
});
}

View File

@@ -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();
};

View 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();
};

View File

@@ -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,
}); });

View File

@@ -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,
}),
);
} }

View File

@@ -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;
} }
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)
); );

View 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;

View File

@@ -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;

View File

@@ -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 } });
}

View File

@@ -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 () => {

View File

@@ -41,6 +41,14 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/carbone/connection' }, { text: 'Connection', link: '/apps/carbone/connection' },
], ],
}, },
{
text: 'Changedetection',
collapsible: true,
collapsed: true,
items: [
{ text: 'Connection', link: '/apps/changedetection/connection' },
],
},
{ {
text: 'Datastore', text: 'Datastore',
collapsible: true, collapsible: true,

View 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.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -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"