From be546b25811136f16dba335c1ab4482d5fe52aa0 Mon Sep 17 00:00:00 2001 From: Santosh Tharu Date: Wed, 27 Mar 2024 21:41:41 +0545 Subject: [PATCH] feat/get-tenant-of-authenticated-user Tasks done --- - Created api endpoint to fetch authenticated user details and its tenant. - Added cors middleware to allow resources to whitelisted domains. --- .env.example | 5 +++- docker-compose.prod.yml | 3 ++ packages/server/package.json | 1 + .../src/api/controllers/Authentication.ts | 29 +++++++++++++++++++ .../src/api/middleware/CorsMiddleware.ts | 18 ++++++++++++ packages/server/src/api/middleware/jwtAuth.ts | 2 ++ packages/server/src/config/index.ts | 5 ++++ packages/server/src/loaders/express.ts | 4 +++ .../Authentication/AuthApplication.ts | 14 +++++++++ .../src/services/Authentication/GetAuthMe.ts | 20 +++++++++++++ .../src/services/Tenancy/TenantService.ts | 21 ++++++++++++++ pnpm-lock.yaml | 3 ++ 12 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/api/middleware/CorsMiddleware.ts create mode 100644 packages/server/src/services/Authentication/GetAuthMe.ts diff --git a/.env.example b/.env.example index f6f3a0d34..60006075d 100644 --- a/.env.example +++ b/.env.example @@ -109,4 +109,7 @@ OIDC_CLIENT_ID= OIDC_CLIENT_SECRET= OIDC_REDIRECT_URI= OIDC_SCOPE= -OIDC_JWK_URI= \ No newline at end of file +OIDC_JWK_URI= + +#CORS +CORS_ALLOWED_DOMAINS=http://localhost:4000,http://localhost:5000 \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b387d1317..1791b3261 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -107,6 +107,9 @@ services: - OIDC_SCOPE=${OIDC_SCOPE} - OIDC_JWK_URI=${OIDC_JWK_URI} + #CORS + - CORS_ALLOWED_DOMAINS=${CORS_ALLOWED_DOMAINS} + database_migration: container_name: bigcapital-database-migration build: diff --git a/packages/server/package.json b/packages/server/package.json index 235315b9e..6d322b01f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -38,6 +38,7 @@ "bluebird": "^3.7.2", "body-parser": "^1.20.2", "compression": "^1.7.4", + "cors": "^2.8.5", "country-codes-list": "^1.6.8", "cpy": "^8.1.2", "cpy-cli": "^3.1.1", diff --git a/packages/server/src/api/controllers/Authentication.ts b/packages/server/src/api/controllers/Authentication.ts index aae43f4b4..c26da733e 100644 --- a/packages/server/src/api/controllers/Authentication.ts +++ b/packages/server/src/api/controllers/Authentication.ts @@ -8,6 +8,8 @@ import { ServiceError, ServiceErrors } from '@/exceptions'; import { DATATYPES_LENGTH } from '@/data/DataTypes'; import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware'; import AuthenticationApplication from '@/services/Authentication/AuthApplication'; +import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser'; +import JWTAuth from '@/api/middleware/jwtAuth'; @Service() export default class AuthenticationController extends BaseController { @@ -50,6 +52,12 @@ export default class AuthenticationController extends BaseController { this.handlerErrors ); router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this))); + + router.use(JWTAuth); + router.use(AttachCurrentTenantUser); + + router.get('/me', asyncMiddleware(this.getAuthMe.bind(this))); + return router; } @@ -226,6 +234,27 @@ export default class AuthenticationController extends BaseController { } } + /** + * Retrieves the authentication user and its tenant + * @param {Request} req + * @param {Response} res + * @param {Function} next + * @returns {Response|void} + */ + private async getAuthMe(req: Request, res: Response, next: Function) { + try { + const { user } = req; + + const tenantId = user.tenantId; + + const tenant = await this.authApplication.getAuthTenant(tenantId); + + return res.status(200).send({ user, tenant }); + } catch (error) { + next(error); + } + } + /** * Handles the service errors. */ diff --git a/packages/server/src/api/middleware/CorsMiddleware.ts b/packages/server/src/api/middleware/CorsMiddleware.ts new file mode 100644 index 000000000..68deb8df9 --- /dev/null +++ b/packages/server/src/api/middleware/CorsMiddleware.ts @@ -0,0 +1,18 @@ +import cors from 'cors'; +import config from '@/config'; + +const corsMiddleware = cors({ + origin: function (origin, callback) { + const allowedDomains = config.cors.whitelistedDomains; + + const requestOrigin = origin?.endsWith('/') ? origin?.slice(0, -1) : origin; + + if (allowedDomains.indexOf(requestOrigin) !== -1 || !requestOrigin) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, +}); + +export default corsMiddleware; diff --git a/packages/server/src/api/middleware/jwtAuth.ts b/packages/server/src/api/middleware/jwtAuth.ts index 9459b695b..3acfb51df 100644 --- a/packages/server/src/api/middleware/jwtAuth.ts +++ b/packages/server/src/api/middleware/jwtAuth.ts @@ -35,6 +35,8 @@ const authMiddleware = (req: Request, res: Response, next: NextFunction) => { const systemUser = await systemUserRepository.findOneByEmail(email); + if (!systemUser) throw new Error(`User with email: ${email} not found`); + const payload = { id: systemUser.id, oidc_access_token: token, diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index 9cf0adc8d..add4aa657 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -195,4 +195,9 @@ module.exports = { oidcLogin: { disabled: parseBoolean(process.env.OIDC_LOGIN_DISABLED, false), }, + cors: { + whitelistedDomains: castCommaListEnvVarToArray( + process.env.CORS_ALLOWED_DOMAINS + ), + }, }; diff --git a/packages/server/src/loaders/express.ts b/packages/server/src/loaders/express.ts index 70b4fd309..212299ce0 100644 --- a/packages/server/src/loaders/express.ts +++ b/packages/server/src/loaders/express.ts @@ -18,6 +18,7 @@ import { import config from '@/config'; import path from 'path'; import ObjectionErrorHandlerMiddleware from '@/api/middleware/ObjectionErrorHandlerMiddleware'; +import corsMiddleware from '@/api/middleware/CorsMiddleware'; export default ({ app }) => { // Express configuration. @@ -30,6 +31,9 @@ export default ({ app }) => { // Helmet helps you secure your Express apps by setting various HTTP headers. app.use(helmet()); + // Cors middleware. + app.use(corsMiddleware); + // Allow to full error stack traces and internal details app.use(errorHandler()); diff --git a/packages/server/src/services/Authentication/AuthApplication.ts b/packages/server/src/services/Authentication/AuthApplication.ts index 9fa74c973..a3cf0fc31 100644 --- a/packages/server/src/services/Authentication/AuthApplication.ts +++ b/packages/server/src/services/Authentication/AuthApplication.ts @@ -4,11 +4,13 @@ import { ISystemUser, IPasswordReset, IAuthGetMetaPOJO, + ITenant, } from '@/interfaces'; import { AuthSigninService } from './AuthSignin'; import { AuthSignupService } from './AuthSignup'; import { AuthSendResetPassword } from './AuthSendResetPassword'; import { GetAuthMeta } from './GetAuthMeta'; +import { GetAuthMe } from './GetAuthMe'; @Service() export default class AuthenticationApplication { @@ -24,6 +26,9 @@ export default class AuthenticationApplication { @Inject() private authGetMeta: GetAuthMeta; + @Inject() + private authGetMe: GetAuthMe; + /** * Signin and generates JWT token. * @throws {ServiceError} @@ -70,4 +75,13 @@ export default class AuthenticationApplication { public async getAuthMeta(): Promise { return this.authGetMeta.getAuthMeta(); } + + /** + * Retrieves the authenticated tenant + * @param {number} tenantId + * @returns {Promise} + */ + public async getAuthTenant(tenantId: number): Promise { + return this.authGetMe.getAuthTenant(tenantId); + } } diff --git a/packages/server/src/services/Authentication/GetAuthMe.ts b/packages/server/src/services/Authentication/GetAuthMe.ts new file mode 100644 index 000000000..bc3915ea4 --- /dev/null +++ b/packages/server/src/services/Authentication/GetAuthMe.ts @@ -0,0 +1,20 @@ +import { Inject, Service } from 'typedi'; +import { ITenant } from '@/interfaces'; +import { TenantService } from '@/services/Tenancy/TenantService'; + +@Service() +export class GetAuthMe { + @Inject() + private tenantService: TenantService; + + /** + * Retrieves the authenticated tenant. + * @param {number} tenantId + * @returns {Promise} + */ + public async getAuthTenant(tenantId: number): Promise { + const tenant = await this.tenantService.getTenantById(tenantId); + + return tenant; + } +} diff --git a/packages/server/src/services/Tenancy/TenantService.ts b/packages/server/src/services/Tenancy/TenantService.ts index e69de29bb..09d4fa899 100644 --- a/packages/server/src/services/Tenancy/TenantService.ts +++ b/packages/server/src/services/Tenancy/TenantService.ts @@ -0,0 +1,21 @@ +import { Service } from 'typedi'; +import { Tenant } from '@/system/models'; +import ModelEntityNotFound from '@/exceptions/ModelEntityNotFound'; + +@Service() +export class TenantService { + /** + * Retrieves tenant by id. + * @param {number} tenantId + * @returns {Promise} + */ + public async getTenantById(tenantId: number): Promise { + const tenant = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + if (!tenant) throw new ModelEntityNotFound(tenantId); + + return tenant; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a481b5128..5e304b316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: compression: specifier: ^1.7.4 version: 1.7.4 + cors: + specifier: ^2.8.5 + version: 2.8.5 country-codes-list: specifier: ^1.6.8 version: 1.6.10