diff --git a/compose.ci.yaml b/compose.ci.yaml
index f803c6b0..fd4250fb 100644
--- a/compose.ci.yaml
+++ b/compose.ci.yaml
@@ -18,6 +18,9 @@ services:
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_NAME: keijo_test
+ ATLASSIAN_CLIENT_ID: mock_id
+ ATLASSIAN_CLIENT_SECRET: mock_secret
+ SESSION_SECRET: mock_secret
ports:
- 4000:3001
diff --git a/compose.yaml b/compose.yaml
index 4e4a2029..72124636 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -28,6 +28,9 @@ services:
service: server-base
container_name: keijo-server
environment:
+ CORS_ORIGIN: http://localhost:3000
+ CALLBACK_URL: http://localhost:3001/jira/callback
+ CALLBACK_REDIRECT_URL: http://localhost:3000
DATABASE_NAME: keijo_dev
ports:
- 3001:3001
@@ -40,6 +43,8 @@ services:
environment:
NETVISOR_API_URL: http://nv-mock:4002
DATABASE_NAME: keijo_test
+ CORS_ORIGIN: http://localhost:4000
+
ports:
- 4001:3001
diff --git a/docs/configuration.md b/docs/configuration.md
index 507c0066..7c5a6caa 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -31,5 +31,14 @@ Key|Default|Description
`DATABASE_HOST`||Postgres hostname.
`DATABASE_PORT`||Postgres port.
`DATABASE_SSL_MODE`|`false`|When set to `true`, TypeORM is configured to use SSL mode `no-verify` when connecting to Postgres. Otherwise, SSL is disabled. See [`node-postgres` docs](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string#tcp-connections) for more.
+`ATLASSIAN_CLIENT_ID`|| Client ID of your Atlassian OAuth 2.0 App. See [Jira OAuth2.0 apps](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/).
+`ATLASSIAN_CLIENT_SECRET`|| Client Secret of your Atlassian OAuth 2.0 App.
+`ATLASSIAN_AUTHORIZATION_URL`|https://auth.atlassian.com/authorize| Atlassian URL where user is directed to grant keijo access to use resources.
+`ATLASSIAN_TOKEN_URL`|https://auth.atlassian.com/oauth/token| Atlassian URL where keijo exchanges authorization code for access and refresh tokens.
+`CALLBACK_URL`|/jira/callback| Keijo URL where user is redirected from Jira after access is granted.
+`CALLBACK_REDIRECT_URL`|/| Keijo URL where user is redirected from callback after callback is handled.
+`SESSION_SECRET`|| Session secret.
+`CORS_ORIGIN`|| Development Cors origin URL.
+`TRUST_PROXY_IPS`|false| Trusted proxy ips to allow secure cookies to be sent over proxies.
1) See [Logging docs](./logging.md) for more information.
diff --git a/docs/development-handbook.md b/docs/development-handbook.md
index 941a4962..8dba70d3 100644
--- a/docs/development-handbook.md
+++ b/docs/development-handbook.md
@@ -121,3 +121,21 @@ Keijo is released as a single Docker image that contains the whole software. (Th
1. Make sure you are in `main` branch and the working tree is clean.
2. Run `npm version `. **Always follow the [semantic versioning guidelines](https://semver.org/).** This script uses `npm version` to apply the desired version bump, sync the sub-projects with it, and finally push the created tags to GitHub.
3. Allow the [CI/CD pipeline](https://github.com/funidata/keijo/actions) to finish. The new version is automatically published once the workflow completes.
+
+## Jira Integration
+
+Keijo integrates to Jira using OAuth2.0 to get, for example, issue related data.
+The backend functions as a [token-mediating backend](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#name-token-mediating-backend), storing users Jira tokens in user sessions, and handing access token to the frontend to make
+requests to [Jira Cloud REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v2).
+
+### Atlassian App
+
+An Atlassian OAuth2.0 app needs to be created to get necessary Atlassian credentials and values.
+Atlassian app can be created in the [developer console](https://developer.atlassian.com/console/myapps/).
+The app needs to be configured to have:
+
+- scope `read:jira-work` (permissions tab).
+- callback URL e.g., `https:///jira/callback` or for local development e.g., `http://localhost:3001/jira/callback` (Authorization tab).
+
+Once these are added to the app, get the client ID and client sercret from app settings.
+For more detailed steps and information see [Jira OAuth2.0 apps](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/).
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index 5db3fbc2..702feecd 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -1,7 +1,7 @@
import { defineConfig, devices } from "@playwright/test";
import { test as base } from "@playwright/test";
import { i18nFixture } from "./i18n-fixture";
-import dayjs, { Dayjs } from "dayjs";
+import dayjs from "dayjs";
import weekOfYear from "dayjs/plugin/weekOfYear";
import localizedFormat from "dayjs/plugin/localizedFormat";
import "dayjs/locale/en-gb";
@@ -11,14 +11,12 @@ import "dayjs/locale/fi";
*/
export default defineConfig({
testDir: "./tests",
- /* Run tests in files in parallel */
- fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
- retries: process.env.CI ? 2 : 0,
+ retries: 2,
/* Opt out of parallel tests on CI. */
- workers: process.env.CI ? 1 : undefined,
+ workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
diff --git a/e2e/tests/entry.mobile.spec.ts b/e2e/tests/entry.mobile.spec.ts
index 005b6c61..474d36a0 100644
--- a/e2e/tests/entry.mobile.spec.ts
+++ b/e2e/tests/entry.mobile.spec.ts
@@ -114,20 +114,8 @@ test.describe("Entry defaults mobile", () => {
});
test("Should use set default values", async ({ page, t }) => {
- await page.goto(emptyWeekUrl + "/create");
- await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue("");
- await page.goto(emptyWeekUrl);
- await page.getByRole("banner").getByLabel(t("controls.openMenu")).click();
- await page.getByRole("button", { name: t("controls.defaultsView") }).click();
- await page.getByRole("combobox", { name: t("entryDialog.product") }).fill(entries[0].product);
- await page.getByRole("combobox", { name: t("entryDialog.activity") }).fill(entries[0].activity);
- await page.goto(emptyWeekUrl + "/create");
- await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue(
- entries[0].product,
- );
- await expect(page.getByRole("combobox", { name: t("entryDialog.activity") })).toHaveValue(
- entries[0].activity,
- );
+ await setDefaultValuesMobile(page, t, entries[0].product, entries[0].activity);
+ await setDefaultValuesMobile(page, t, productNames[1], activityNames[1]);
});
});
@@ -140,3 +128,23 @@ const fillEntryFormMobile = async (page: Page, t: TFunction, entry: TestEntry) =
await page.getByRole("gridcell", { name: entry.date.split(".")[0] }).click();
await page.getByRole("button", { name: "OK" }).click();
};
+
+const setDefaultValuesMobile = async (
+ page: Page,
+ t: TFunction,
+ product: string,
+ activity: string,
+) => {
+ await page.goto(emptyWeekUrl);
+ await page.getByRole("banner").getByLabel(t("controls.openMenu")).click();
+ await page.getByRole("button", { name: t("controls.defaultsView") }).click();
+ await page.getByRole("combobox", { name: t("entryDialog.product") }).click();
+ await page.getByRole("option", { name: product }).click();
+ await page.getByRole("combobox", { name: t("entryDialog.activity") }).click();
+ await page.getByRole("option", { name: activity }).click();
+ await page.goto(emptyWeekUrl + "/create");
+ await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue(product);
+ await expect(page.getByRole("combobox", { name: t("entryDialog.activity") })).toHaveValue(
+ activity,
+ );
+};
diff --git a/e2e/tests/entry.spec.ts b/e2e/tests/entry.spec.ts
index 7ee36d77..a62ec612 100644
--- a/e2e/tests/entry.spec.ts
+++ b/e2e/tests/entry.spec.ts
@@ -115,25 +115,8 @@ test.describe("Entry defaults", () => {
});
test("Should use set default values", async ({ page, t }) => {
- await page.goto(emptyWeekUrl + "/create");
- await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue("");
- await page.goto(emptyWeekUrl);
-
- await page.getByLabel(t("controls.settingsMenu")).click();
- await page.getByRole("menuitem", { name: t("controls.defaultsView") }).click();
-
- await page.getByRole("combobox", { name: t("entryDialog.product") }).click();
- await page.getByRole("option", { name: entries[0].product }).click();
- await page.getByRole("combobox", { name: t("entryDialog.activity") }).click();
- await page.getByRole("option", { name: entries[0].activity }).click();
-
- await page.goto(emptyWeekUrl + "/create");
- await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue(
- entries[0].product,
- );
- await expect(page.getByRole("combobox", { name: t("entryDialog.activity") })).toHaveValue(
- entries[0].activity,
- );
+ await setDefaultValues(page, t, entries[0].product, entries[0].activity);
+ await setDefaultValues(page, t, productNames[1], activityNames[1]);
});
});
@@ -148,3 +131,20 @@ const fillEntryForm = async (page: Page, t: TFunction, entry: TestEntry) => {
.first()
.click();
};
+
+const setDefaultValues = async (page: Page, t: TFunction, product: string, activity: string) => {
+ await page.goto(emptyWeekUrl);
+ await page.getByLabel(t("controls.settingsMenu")).click();
+ await page.getByRole("menuitem", { name: t("controls.defaultsView") }).click();
+
+ await page.getByRole("combobox", { name: t("entryDialog.product") }).click();
+ await page.getByRole("option", { name: product }).click();
+ await page.getByRole("combobox", { name: t("entryDialog.activity") }).click();
+ await page.getByRole("option", { name: activity }).click();
+
+ await page.goto(emptyWeekUrl + "/create");
+ await expect(page.getByRole("combobox", { name: t("entryDialog.product") })).toHaveValue(product);
+ await expect(page.getByRole("combobox", { name: t("entryDialog.activity") })).toHaveValue(
+ activity,
+ );
+};
diff --git a/server/package-lock.json b/server/package-lock.json
index 8a560466..41be0a45 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -14,6 +14,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.0.8",
+ "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/typeorm": "^10.0.2",
@@ -21,10 +22,15 @@
"cache-manager": "^5.2.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
+ "connect-pg-simple": "^9.0.1",
+ "cookie-parser": "^1.4.6",
"dayjs": "^1.11.9",
+ "express-session": "^1.18.0",
"fast-xml-parser": "^4.2.7",
"graphql": "^16.8.0",
"lodash": "^4.17.21",
+ "passport": "^0.7.0",
+ "passport-oauth2": "^1.8.0",
"pg": "^8.12.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -38,10 +44,14 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
+ "@types/connect-pg-simple": "^7.0.3",
+ "@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
+ "@types/express-session": "^1.18.0",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^20.3.1",
+ "@types/passport-oauth2": "^1.4.17",
"@types/sha.js": "^2.4.1",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.2",
@@ -2196,6 +2206,15 @@
}
}
},
+ "node_modules/@nestjs/passport": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz",
+ "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==",
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0"
+ }
+ },
"node_modules/@nestjs/platform-express": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.6.tgz",
@@ -2589,6 +2608,26 @@
"@types/node": "*"
}
},
+ "node_modules/@types/connect-pg-simple": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/connect-pg-simple/-/connect-pg-simple-7.0.3.tgz",
+ "integrity": "sha512-NGCy9WBlW2bw+J/QlLnFZ9WjoGs6tMo3LAut6mY4kK+XHzue//lpNVpAvYRpIwM969vBRAM2Re0izUvV6kt+NA==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/express-session": "*",
+ "@types/pg": "*"
+ }
+ },
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -2643,6 +2682,15 @@
"@types/send": "*"
}
},
+ "node_modules/@types/express-session": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz",
+ "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -2740,12 +2788,109 @@
"node": ">= 6"
}
},
+ "node_modules/@types/oauth": {
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.5.tgz",
+ "integrity": "sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "node_modules/@types/passport": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
+ "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/passport-oauth2": {
+ "version": "1.4.17",
+ "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz",
+ "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/oauth": "*",
+ "@types/passport": "*"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.11.6",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
+ "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^4.0.1"
+ }
+ },
+ "node_modules/@types/pg/node_modules/pg-types": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
+ "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
+ "dev": true,
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "pg-numeric": "1.0.2",
+ "postgres-array": "~3.0.1",
+ "postgres-bytea": "~3.0.0",
+ "postgres-date": "~2.1.0",
+ "postgres-interval": "^3.0.0",
+ "postgres-range": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@types/pg/node_modules/postgres-array": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
+ "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@types/pg/node_modules/postgres-bytea": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
+ "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
+ "dev": true,
+ "dependencies": {
+ "obuf": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@types/pg/node_modules/postgres-date": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
+ "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@types/pg/node_modules/postgres-interval": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
+ "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@@ -3541,6 +3686,14 @@
}
]
},
+ "node_modules/base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -4119,6 +4272,17 @@
"typedarray": "^0.0.6"
}
},
+ "node_modules/connect-pg-simple": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-9.0.1.tgz",
+ "integrity": "sha512-BuwWJH3K3aLpONkO9s12WhZ9ceMjIBxIJAh0JD9x4z1Y9nShmWqZvge5PG/+4j2cIOcguUoa2PSQ4HO/oTsrVg==",
+ "dependencies": {
+ "pg": "^8.8.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@@ -4157,6 +4321,26 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "dependencies": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -4799,6 +4983,42 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-session": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
+ "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
+ "dependencies": {
+ "cookie": "0.6.0",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -6949,6 +7169,11 @@
"node": ">=8"
}
},
+ "node_modules/oauth": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
+ "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6965,6 +7190,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "dev": true
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -6976,6 +7207,14 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -7160,6 +7399,50 @@
"node": ">= 0.8"
}
},
+ "node_modules/passport": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1",
+ "utils-merge": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-oauth2": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
+ "dependencies": {
+ "base64url": "3.x.x",
+ "oauth": "0.10.x",
+ "passport-strategy": "1.x.x",
+ "uid2": "0.0.x",
+ "utils-merge": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -7237,6 +7520,11 @@
"node": ">=8"
}
},
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ },
"node_modules/pg": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
@@ -7282,6 +7570,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/pg-numeric": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
+ "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/pg-pool": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
@@ -7452,6 +7749,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/postgres-range": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
+ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
+ "dev": true
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7590,6 +7893,14 @@
}
]
},
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -9086,6 +9397,22 @@
"node": ">=8"
}
},
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uid2": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
+ },
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
diff --git a/server/package.json b/server/package.json
index dd7d1ccb..c94d7d1f 100644
--- a/server/package.json
+++ b/server/package.json
@@ -27,6 +27,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.0.8",
+ "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/typeorm": "^10.0.2",
@@ -34,10 +35,15 @@
"cache-manager": "^5.2.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
+ "connect-pg-simple": "^9.0.1",
+ "cookie-parser": "^1.4.6",
"dayjs": "^1.11.9",
+ "express-session": "^1.18.0",
"fast-xml-parser": "^4.2.7",
"graphql": "^16.8.0",
"lodash": "^4.17.21",
+ "passport": "^0.7.0",
+ "passport-oauth2": "^1.8.0",
"pg": "^8.12.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -51,10 +57,14 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
+ "@types/connect-pg-simple": "^7.0.3",
+ "@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
+ "@types/express-session": "^1.18.0",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^20.3.1",
+ "@types/passport-oauth2": "^1.4.17",
"@types/sha.js": "^2.4.1",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.2",
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index ec5e3a52..650e3050 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -12,6 +12,7 @@ import { LoggerModule } from "./logger/logger.module";
import { NetvisorModule } from "./netvisor/netvisor.module";
import { SessionModule } from "./session/session.module";
import { UserSettingsModule } from "./user-settings/user-settings.module";
+import { JiraModule } from "./jira/jira.module";
@Module({
imports: [
@@ -27,6 +28,7 @@ import { UserSettingsModule } from "./user-settings/user-settings.module";
SessionModule,
DatabaseModule,
UserSettingsModule,
+ JiraModule,
],
providers: [...appGuards],
})
diff --git a/server/src/config/config.schema.ts b/server/src/config/config.schema.ts
index 82d964d2..7af6795e 100644
--- a/server/src/config/config.schema.ts
+++ b/server/src/config/config.schema.ts
@@ -27,6 +27,20 @@ export const configSchema = object({
port: number(),
ssl: boolean(),
}),
+ jira: object({
+ tokenUrl: string(),
+ clientId: string(),
+ clientSecret: string(),
+ authorizationUrl: string(),
+ callbackUrl: string(),
+ scopes: string(),
+ callbackRedirectUrl: string(),
+ }),
+ session: object({
+ name: string(),
+ secret: string(),
+ }),
+ trustProxyIps: string().or(boolean()),
});
export type Config = zod.infer;
diff --git a/server/src/config/config.ts b/server/src/config/config.ts
index 539b10e4..0e584197 100644
--- a/server/src/config/config.ts
+++ b/server/src/config/config.ts
@@ -19,6 +19,14 @@ const {
DATABASE_HOST,
DATABASE_PORT,
DATABASE_SSL_MODE,
+ ATLASSIAN_TOKEN_URL,
+ ATLASSIAN_CLIENT_ID,
+ ATLASSIAN_CLIENT_SECRET,
+ ATLASSIAN_AUTHORIZATION_URL,
+ CALLBACK_URL,
+ CALLBACK_REDIRECT_URL,
+ SESSION_SECRET,
+ TRUST_PROXY_IPS,
} = process.env;
const config = {
@@ -49,6 +57,20 @@ const config = {
port: Number(DATABASE_PORT),
ssl: DATABASE_SSL_MODE === "true",
},
+ jira: {
+ tokenUrl: ATLASSIAN_TOKEN_URL || "https://auth.atlassian.com/oauth/token",
+ clientId: ATLASSIAN_CLIENT_ID,
+ clientSecret: ATLASSIAN_CLIENT_SECRET,
+ authorizationUrl: ATLASSIAN_AUTHORIZATION_URL || "https://auth.atlassian.com/authorize",
+ callbackUrl: CALLBACK_URL || "/jira/callback",
+ callbackRedirectUrl: CALLBACK_REDIRECT_URL || "/",
+ scopes: "read:jira-work offline_access",
+ },
+ session: {
+ secret: SESSION_SECRET,
+ name: "sessionId",
+ },
+ trustProxyIps: TRUST_PROXY_IPS || false,
};
export default config;
diff --git a/server/src/jira/jira.controller.ts b/server/src/jira/jira.controller.ts
new file mode 100644
index 00000000..6bc6defc
--- /dev/null
+++ b/server/src/jira/jira.controller.ts
@@ -0,0 +1,46 @@
+import { Controller, Get, Res, UseGuards } from "@nestjs/common";
+import { Response } from "express";
+import { BypassHeadersGuard } from "../decorators/bypass-headers-guard.decorator";
+import { JiraAuthGuard } from "./jira.guard";
+import { JiraService } from "./jira.service";
+import { SessionTokenGuard } from "./token.guard";
+import { ConfigService } from "../config/config.service";
+import { JiraTokens } from "./jira.types";
+import { SessionUser, ReqUser } from "./user.decorator";
+
+@Controller("jira")
+export class JiraController {
+ constructor(
+ private jiraService: JiraService,
+ private configService: ConfigService,
+ ) {}
+
+ @BypassHeadersGuard()
+ @UseGuards(JiraAuthGuard)
+ @Get()
+ jiraAuthRedirect() {}
+
+ @BypassHeadersGuard()
+ @UseGuards(JiraAuthGuard)
+ @Get("callback")
+ async handleRedirect(@Res() response: Response, @ReqUser() jiraTokens: JiraTokens) {
+ this.jiraService.setJiraSessionTokens(jiraTokens);
+ response.redirect(301, this.configService.config.jira.callbackRedirectUrl);
+ }
+
+ @BypassHeadersGuard()
+ @UseGuards(SessionTokenGuard)
+ @Get("refresh")
+ async refreshTokens(@SessionUser() jiraTokens: JiraTokens) {
+ const freshTokens = await this.jiraService.getFreshTokens(jiraTokens.refreshToken);
+ this.jiraService.setJiraSessionTokens(freshTokens);
+ return { access_token: freshTokens.accessToken };
+ }
+
+ @BypassHeadersGuard()
+ @UseGuards(SessionTokenGuard)
+ @Get("access-token")
+ async getAccessToken(@SessionUser() jiraTokens: JiraTokens) {
+ return { access_token: jiraTokens.accessToken };
+ }
+}
diff --git a/server/src/jira/jira.guard.ts b/server/src/jira/jira.guard.ts
new file mode 100644
index 00000000..66833df2
--- /dev/null
+++ b/server/src/jira/jira.guard.ts
@@ -0,0 +1,5 @@
+import { Injectable } from "@nestjs/common";
+import { AuthGuard } from "@nestjs/passport";
+
+@Injectable()
+export class JiraAuthGuard extends AuthGuard("jira") {}
diff --git a/server/src/jira/jira.module.ts b/server/src/jira/jira.module.ts
new file mode 100644
index 00000000..dde0bf94
--- /dev/null
+++ b/server/src/jira/jira.module.ts
@@ -0,0 +1,13 @@
+import { Module } from "@nestjs/common";
+import { JiraController } from "./jira.controller";
+import { JiraStrategy } from "./jira.strategy";
+import { AxiosModule } from "../axios/axios.module";
+import { JiraService } from "./jira.service";
+import { PassportModule } from "@nestjs/passport";
+
+@Module({
+ imports: [AxiosModule, PassportModule.register({ session: true })],
+ controllers: [JiraController],
+ providers: [JiraStrategy, JiraService],
+})
+export class JiraModule {}
diff --git a/server/src/jira/jira.service.ts b/server/src/jira/jira.service.ts
new file mode 100644
index 00000000..2a4633ab
--- /dev/null
+++ b/server/src/jira/jira.service.ts
@@ -0,0 +1,36 @@
+import { Inject, Injectable, Scope } from "@nestjs/common";
+import { AxiosService } from "../axios/axios.service";
+import { ConfigService } from "../config/config.service";
+import { JiraTokens } from "./jira.types";
+import { REQUEST } from "@nestjs/core";
+import { Request } from "express";
+
+@Injectable({ scope: Scope.REQUEST })
+export class JiraService {
+ constructor(
+ private axiosService: AxiosService,
+ private configService: ConfigService,
+ @Inject(REQUEST) private request: Request,
+ ) {}
+
+ async getFreshTokens(refreshToken: string): Promise {
+ const { clientId, clientSecret, tokenUrl } = this.configService.config.jira;
+ const res = await this.axiosService.post<{ refresh_token: string; access_token: string }>(
+ tokenUrl,
+ {
+ grant_type: "refresh_token",
+ client_id: clientId,
+ client_secret: clientSecret,
+ refresh_token: refreshToken,
+ },
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ return { refreshToken: res.data.refresh_token, accessToken: res.data.access_token };
+ }
+
+ setJiraSessionTokens(jiraTokens: JiraTokens) {
+ this.request.session.user = jiraTokens;
+ }
+}
diff --git a/server/src/jira/jira.strategy.ts b/server/src/jira/jira.strategy.ts
new file mode 100644
index 00000000..4c8cd30c
--- /dev/null
+++ b/server/src/jira/jira.strategy.ts
@@ -0,0 +1,33 @@
+import { Strategy, StrategyOptions } from "passport-oauth2";
+import { PassportStrategy } from "@nestjs/passport";
+import { Injectable } from "@nestjs/common";
+import { ConfigService } from "../config/config.service";
+import { JiraTokens } from "./jira.types";
+
+@Injectable()
+export class JiraStrategy extends PassportStrategy(Strategy, "jira") {
+ constructor(configService: ConfigService) {
+ const { authorizationUrl, callbackUrl, clientId, clientSecret, tokenUrl, scopes } =
+ configService.config.jira;
+ super({
+ authorizationURL: authorizationUrl,
+ callbackURL: callbackUrl,
+ clientID: clientId,
+ clientSecret: clientSecret,
+ tokenURL: tokenUrl,
+ state: true,
+ scope: scopes,
+ });
+ }
+
+ authorizationParams() {
+ return {
+ audience: "api.atlassian.com",
+ prompt: "consent",
+ };
+ }
+
+ async validate(accessToken: string, refreshToken: string): Promise {
+ return { accessToken, refreshToken };
+ }
+}
diff --git a/server/src/jira/jira.types.ts b/server/src/jira/jira.types.ts
new file mode 100644
index 00000000..95adb8e6
--- /dev/null
+++ b/server/src/jira/jira.types.ts
@@ -0,0 +1,11 @@
+export type JiraTokens = { refreshToken: string; accessToken: string };
+export interface JiraUser {
+ user?: JiraTokens;
+}
+
+declare module "express-session" {
+ interface SessionData extends JiraUser {}
+}
+declare module "express" {
+ export interface Request extends JiraUser {}
+}
diff --git a/server/src/jira/session-setup.ts b/server/src/jira/session-setup.ts
new file mode 100644
index 00000000..a7c2a884
--- /dev/null
+++ b/server/src/jira/session-setup.ts
@@ -0,0 +1,31 @@
+import session from "express-session";
+import pgSession from "connect-pg-simple";
+import pg from "pg";
+import { ConfigService } from "../config/config.service";
+
+export function createSession(configService: ConfigService) {
+ const { username, password, host, port, name } = configService.config.database;
+ const pgPool = new pg.Pool({
+ database: name,
+ user: username,
+ password,
+ host,
+ port,
+ });
+
+ return session({
+ store: new (pgSession(session))({
+ pool: pgPool,
+ createTableIfMissing: configService.config.inDev,
+ }),
+ name: configService.config.session.name,
+ secret: configService.config.session.secret,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ httpOnly: true,
+ secure: !configService.config.inDev,
+ maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days
+ },
+ });
+}
diff --git a/server/src/jira/token.guard.ts b/server/src/jira/token.guard.ts
new file mode 100644
index 00000000..90c8c3ec
--- /dev/null
+++ b/server/src/jira/token.guard.ts
@@ -0,0 +1,16 @@
+import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
+import { Request } from "express";
+
+@Injectable()
+export class SessionTokenGuard implements CanActivate {
+ async canActivate(context: ExecutionContext) {
+ const request = context.switchToHttp().getRequest();
+ const session = request.session;
+
+ if (session.user?.refreshToken && session.user.accessToken) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/server/src/jira/user.decorator.ts b/server/src/jira/user.decorator.ts
new file mode 100644
index 00000000..da20c9ad
--- /dev/null
+++ b/server/src/jira/user.decorator.ts
@@ -0,0 +1,14 @@
+import { createParamDecorator, ExecutionContext } from "@nestjs/common";
+import { Request } from "express";
+import { SessionData } from "express-session";
+
+export const SessionUser = createParamDecorator((_, ctx: ExecutionContext) => {
+ const request: Request = ctx.switchToHttp().getRequest();
+ const session: SessionData = request.session;
+ return session.user;
+});
+
+export const ReqUser = createParamDecorator((_, ctx: ExecutionContext) => {
+ const request: Request = ctx.switchToHttp().getRequest();
+ return request.user;
+});
diff --git a/server/src/main.ts b/server/src/main.ts
index ad98f8d2..4d61674c 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -1,17 +1,34 @@
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
+import { NestExpressApplication } from "@nestjs/platform-express";
import { useContainer } from "class-validator";
import { AppModule } from "./app.module";
import { ConfigService } from "./config/config.service";
import { AppLogger } from "./logger/app-logger";
+import cookieParser from "cookie-parser";
+import { createSession } from "./jira/session-setup";
-const options = process.env.NODE_ENV === "development" ? { cors: { origin: "*" } } : {};
+const options =
+ process.env.NODE_ENV === "development"
+ ? {
+ cors: {
+ origin: process.env.CORS_ORIGIN,
+ credentials: true,
+ },
+ }
+ : {};
(async () => {
- const app = await NestFactory.create(AppModule, { bufferLogs: true, ...options });
+ const app = await NestFactory.create(AppModule, {
+ bufferLogs: true,
+ ...options,
+ });
const configService = app.get(ConfigService);
app.useLogger(new AppLogger(configService));
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe());
+ app.set("trust proxy", configService.config.trustProxyIps);
+ app.use(createSession(configService));
+ app.use(cookieParser());
await app.listen(configService.config.port);
})();
diff --git a/server/test/utils/mock-config.service.ts b/server/test/utils/mock-config.service.ts
index e3a7c2e3..c7f0e82e 100644
--- a/server/test/utils/mock-config.service.ts
+++ b/server/test/utils/mock-config.service.ts
@@ -28,6 +28,20 @@ export const defaultMockConfig: Config = {
port: 0,
ssl: false,
},
+ jira: {
+ tokenUrl: "test",
+ clientId: "test",
+ clientSecret: "test",
+ authorizationUrl: "test",
+ callbackUrl: "test",
+ callbackRedirectUrl: "test",
+ scopes: "test",
+ },
+ session: {
+ name: "test",
+ secret: "test",
+ },
+ trustProxyIps: false,
};
const mockConfigProvider = (override: Partial = {}) => ({