mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
⚡ Make n8n work in subfolder & Fix events in AffinityTrigger
This commit is contained in:
parent
c08ff6cee3
commit
941ee06b14
25
docker/compose/subfolderWithSSL/.env
Normal file
25
docker/compose/subfolderWithSSL/.env
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Folder where data should be saved
|
||||||
|
DATA_FOLDER=/root/n8n/
|
||||||
|
|
||||||
|
# The top level domain to serve from
|
||||||
|
DOMAIN_NAME=example.com
|
||||||
|
|
||||||
|
# The subfolder to serve from
|
||||||
|
SUBFOLDER=app1
|
||||||
|
N8N_PATH=/app1/
|
||||||
|
|
||||||
|
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
|
||||||
|
# above example would result in: https://example.com/n8n/
|
||||||
|
|
||||||
|
# The user name to use for autentication - IMPORTANT ALWAYS CHANGE!
|
||||||
|
N8N_BASIC_AUTH_USER=user
|
||||||
|
|
||||||
|
# The password to use for autentication - IMPORTANT ALWAYS CHANGE!
|
||||||
|
N8N_BASIC_AUTH_PASSWORD=password
|
||||||
|
|
||||||
|
# Optional timezone to set which gets used by Cron-Node by default
|
||||||
|
# If not set New York time will be used
|
||||||
|
GENERIC_TIMEZONE=Europe/Berlin
|
||||||
|
|
||||||
|
# The email address to use for the SSL certificate creation
|
||||||
|
SSL_EMAIL=user@example.com
|
26
docker/compose/subfolderWithSSL/README.md
Normal file
26
docker/compose/subfolderWithSSL/README.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# n8n on Subfolder with SSL
|
||||||
|
|
||||||
|
Starts n8n and deployes it on a subfolder
|
||||||
|
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
To start n8n in a subfolder simply start docker-compose by executing the following
|
||||||
|
command in the current folder.
|
||||||
|
|
||||||
|
|
||||||
|
**IMPORTANT:** But before you do that change the default users and passwords in the `.env` file!
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
To stop it execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The default name of the database, user and password for MongoDB can be changed in the `.env` file in the current directory.
|
57
docker/compose/subfolderWithSSL/docker-compose.yml
Normal file
57
docker/compose/subfolderWithSSL/docker-compose.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik"
|
||||||
|
command:
|
||||||
|
- "--api=true"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--api.dashboard=true"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
|
||||||
|
- "--certificatesresolvers.mytlschallenge.acme.email=${SSL_EMAIL}"
|
||||||
|
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- /home/jan/www/n8n/n8n:/data
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ${DATA_FOLDER}/letsencrypt:/letsencrypt
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5678:5678"
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.n8n.rule=Host(`${DOMAIN_NAME}`)
|
||||||
|
- traefik.http.routers.n8n.tls=true
|
||||||
|
- traefik.http.routers.n8n.entrypoints=websecure
|
||||||
|
- "traefik.http.routers.n8n.rule=PathPrefix(`/${SUBFOLDER}{regex:$$|/.*}`)"
|
||||||
|
- "traefik.http.middlewares.n8n-stripprefix.stripprefix.prefixes=/${SUBFOLDER}"
|
||||||
|
- "traefik.http.routers.n8n.middlewares=n8n-stripprefix"
|
||||||
|
- traefik.http.routers.n8n.tls.certresolver=mytlschallenge
|
||||||
|
- traefik.http.middlewares.n8n.headers.SSLRedirect=true
|
||||||
|
- traefik.http.middlewares.n8n.headers.STSSeconds=315360000
|
||||||
|
- traefik.http.middlewares.n8n.headers.browserXSSFilter=true
|
||||||
|
- traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
|
||||||
|
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true
|
||||||
|
- traefik.http.middlewares.n8n.headers.SSLHost=${DOMAIN_NAME}
|
||||||
|
- traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
|
||||||
|
- traefik.http.middlewares.n8n.headers.STSPreload=true
|
||||||
|
environment:
|
||||||
|
- N8N_BASIC_AUTH_ACTIVE=true
|
||||||
|
- N8N_BASIC_AUTH_USER
|
||||||
|
- N8N_BASIC_AUTH_PASSWORD
|
||||||
|
- N8N_HOST=${DOMAIN_NAME}
|
||||||
|
- N8N_PORT=5678
|
||||||
|
- N8N_PROTOCOL=https
|
||||||
|
- NODE_ENV=production
|
||||||
|
- N8N_PATH
|
||||||
|
- WEBHOOK_TUNNEL_URL=http://${DOMAIN_NAME}${N8N_PATH}
|
||||||
|
- VUE_APP_URL_BASE_API=http://${DOMAIN_NAME}${N8N_PATH}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ${DATA_FOLDER}/.n8n:/root/.n8n
|
|
@ -204,6 +204,13 @@ const config = convict({
|
||||||
},
|
},
|
||||||
|
|
||||||
// How n8n can be reached (Editor & REST-API)
|
// How n8n can be reached (Editor & REST-API)
|
||||||
|
path: {
|
||||||
|
format: String,
|
||||||
|
default: '/',
|
||||||
|
arg: 'path',
|
||||||
|
env: 'N8N_PATH',
|
||||||
|
doc: 'Path n8n is deployed to'
|
||||||
|
},
|
||||||
host: {
|
host: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'localhost',
|
default: 'localhost',
|
||||||
|
|
|
@ -40,11 +40,12 @@ export function getBaseUrl(): string {
|
||||||
const protocol = config.get('protocol') as string;
|
const protocol = config.get('protocol') as string;
|
||||||
const host = config.get('host') as string;
|
const host = config.get('host') as string;
|
||||||
const port = config.get('port') as number;
|
const port = config.get('port') as number;
|
||||||
|
const path = config.get('path') as string;
|
||||||
|
|
||||||
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
|
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
|
||||||
return `${protocol}://${host}/`;
|
return `${protocol}://${host}${path}`;
|
||||||
}
|
}
|
||||||
return `${protocol}://${host}:${port}/`;
|
return `${protocol}://${host}:${port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -931,7 +931,8 @@ class App {
|
||||||
// Authorize OAuth Data
|
// Authorize OAuth Data
|
||||||
this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
||||||
if (req.query.id === undefined) {
|
if (req.query.id === undefined) {
|
||||||
throw new Error('Required credential id is missing!');
|
res.status(500).send('Required credential id is missing!');
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
||||||
|
@ -943,7 +944,8 @@ class App {
|
||||||
let encryptionKey = undefined;
|
let encryptionKey = undefined;
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
if (encryptionKey === undefined) {
|
||||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
res.status(500).send('No encryption key got found to decrypt the credentials!');
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the currently saved credentials
|
// Decrypt the currently saved credentials
|
||||||
|
@ -1015,7 +1017,8 @@ class App {
|
||||||
const { oauth_verifier, oauth_token, cid } = req.query;
|
const { oauth_verifier, oauth_token, cid } = req.query;
|
||||||
|
|
||||||
if (oauth_verifier === undefined || oauth_token === undefined) {
|
if (oauth_verifier === undefined || oauth_token === undefined) {
|
||||||
throw new Error('Insufficient parameters for OAuth1 callback');
|
const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth1 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
|
||||||
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
|
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
|
||||||
|
@ -1085,7 +1088,8 @@ class App {
|
||||||
// Authorize OAuth Data
|
// Authorize OAuth Data
|
||||||
this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
||||||
if (req.query.id === undefined) {
|
if (req.query.id === undefined) {
|
||||||
throw new Error('Required credential id is missing!');
|
res.status(500).send('Required credential id is missing.');
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
||||||
|
@ -1097,7 +1101,8 @@ class App {
|
||||||
let encryptionKey = undefined;
|
let encryptionKey = undefined;
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
if (encryptionKey === undefined) {
|
||||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
res.status(500).send('No encryption key got found to decrypt the credentials!');
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the currently saved credentials
|
// Decrypt the currently saved credentials
|
||||||
|
@ -1161,7 +1166,8 @@ class App {
|
||||||
const {code, state: stateEncoded } = req.query;
|
const {code, state: stateEncoded } = req.query;
|
||||||
|
|
||||||
if (code === undefined || stateEncoded === undefined) {
|
if (code === undefined || stateEncoded === undefined) {
|
||||||
throw new Error('Insufficient parameters for OAuth2 callback');
|
const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth2 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
|
||||||
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
@ -1211,17 +1217,20 @@ class App {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const redirectUri = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`;
|
||||||
|
|
||||||
const oAuthObj = new clientOAuth2({
|
const oAuthObj = new clientOAuth2({
|
||||||
clientId: _.get(oauthCredentials, 'clientId') as string,
|
clientId: _.get(oauthCredentials, 'clientId') as string,
|
||||||
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||||
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||||
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
||||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`,
|
redirectUri,
|
||||||
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
|
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
|
||||||
});
|
});
|
||||||
|
|
||||||
const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options);
|
const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
|
||||||
|
|
||||||
|
const oauthToken = await oAuthObj.code.getToken(`${redirectUri}?${queryParameters}`, options);
|
||||||
|
|
||||||
if (oauthToken === undefined) {
|
if (oauthToken === undefined) {
|
||||||
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
|
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
|
||||||
|
@ -1693,9 +1702,21 @@ class App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read the index file and replace the path placeholder
|
||||||
|
const editorUiPath = require.resolve('n8n-editor-ui');
|
||||||
|
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
|
||||||
|
let readIndexFile = readFileSync(filePath, 'utf8');
|
||||||
|
const n8nPath = config.get('path');
|
||||||
|
readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
|
||||||
|
|
||||||
|
// Serve the altered index.html file separately
|
||||||
|
this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
|
||||||
|
res.send(readIndexFile);
|
||||||
|
});
|
||||||
|
|
||||||
// Serve the website
|
// Serve the website
|
||||||
const startTime = (new Date()).toUTCString();
|
const startTime = (new Date()).toUTCString();
|
||||||
const editorUiPath = require.resolve('n8n-editor-ui');
|
|
||||||
this.app.use('/', express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
|
this.app.use('/', express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
|
||||||
index: 'index.html',
|
index: 'index.html',
|
||||||
setHeaders: (res, path) => {
|
setHeaders: (res, path) => {
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="/%BASE_PATH%/favicon.ico">
|
||||||
|
<script type="text/javascript">window.BASE_PATH = "/%BASE_PATH%/";</script>
|
||||||
<title>n8n.io - Workflow Automation</title>
|
<title>n8n.io - Workflow Automation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<el-menu-item index="logo" class="logo-item">
|
<el-menu-item index="logo" class="logo-item">
|
||||||
<a href="https://n8n.io" target="_blank" class="logo">
|
<a href="https://n8n.io" target="_blank" class="logo">
|
||||||
<img src="/n8n-icon-small.png" class="icon" alt="n8n.io"/>
|
<img :src="basePath + 'n8n-icon-small.png'" class="icon" alt="n8n.io"/>
|
||||||
<span class="logo-text" slot="title">n8n.io</span>
|
<span class="logo-text" slot="title">n8n.io</span>
|
||||||
</a>
|
</a>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
@ -208,6 +208,8 @@ export default mixins(
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
aboutDialogVisible: false,
|
aboutDialogVisible: false,
|
||||||
|
// @ts-ignore
|
||||||
|
basePath: window.BASE_PATH,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
credentialNewDialogVisible: false,
|
credentialNewDialogVisible: false,
|
||||||
credentialOpenDialogVisible: false,
|
credentialOpenDialogVisible: false,
|
||||||
|
|
|
@ -8,7 +8,8 @@ Vue.use(Router);
|
||||||
|
|
||||||
export default new Router({
|
export default new Router({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
base: process.env.BASE_URL,
|
// @ts-ignore
|
||||||
|
base: window.BASE_PATH,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/execution/:id',
|
path: '/execution/:id',
|
||||||
|
|
|
@ -38,7 +38,8 @@ export const store = new Vuex.Store({
|
||||||
activeWorkflows: [] as string[],
|
activeWorkflows: [] as string[],
|
||||||
activeActions: [] as string[],
|
activeActions: [] as string[],
|
||||||
activeNode: null as string | null,
|
activeNode: null as string | null,
|
||||||
baseUrl: process.env.VUE_APP_URL_BASE_API ? process.env.VUE_APP_URL_BASE_API : '/',
|
// @ts-ignore
|
||||||
|
baseUrl: window.BASE_PATH ? window.BASE_PATH : '/',
|
||||||
credentials: null as ICredentialsResponse[] | null,
|
credentials: null as ICredentialsResponse[] | null,
|
||||||
credentialTypes: null as ICredentialType[] | null,
|
credentialTypes: null as ICredentialType[] | null,
|
||||||
endpointWebhook: 'webhook',
|
endpointWebhook: 'webhook',
|
||||||
|
|
|
@ -29,5 +29,5 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/',
|
publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/%BASE_PATH%/',
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,10 +52,10 @@ export class AffinityTrigger implements INodeType {
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'file.created',
|
name: 'file.created',
|
||||||
value: 'file.deleted',
|
value: 'file.created',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'file.created',
|
name: 'file.deleted',
|
||||||
value: 'file.deleted',
|
value: 'file.deleted',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -136,7 +136,7 @@ export class AffinityTrigger implements INodeType {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'opportunity.deleted',
|
name: 'opportunity.deleted',
|
||||||
value: 'organization.deleted',
|
value: 'opportunity.deleted',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'person.created',
|
name: 'person.created',
|
||||||
|
|
Loading…
Reference in a new issue