diff --git a/@core/cms/src/app.controller.ts b/@core/cms/src/app.controller.ts index 552be75..5cc0fc9 100644 --- a/@core/cms/src/app.controller.ts +++ b/@core/cms/src/app.controller.ts @@ -8,6 +8,9 @@ import { CreateCredentialDto } from "./database-integration/dto/create-credentia import { ValidateCredentialPipe } from "./common/pipes/validate-credential.pipe"; import { ExternalAuthIntegrationService } from "./external-auth-integration/external-auth-integration.service"; +import { BasicAuthDto } from "./dto/basic-auth.dto"; +import { BasicAuthVerifyDto } from "./dto/basic-auth-verify.dto"; + @Controller() export class AppController { constructor( @@ -15,11 +18,14 @@ export class AppController { private encryptionDecryptionService: EncryptionDecryptionService, private externalAuthIntegrationService: ExternalAuthIntegrationService, ) {} + + private readonly logger = new Logger(AppController.name); + // TODO: Add DTO @MessagePattern("create_credential") @UsePipes(new ValidateCredentialPipe()) async handleCreateCredential(message: CreateCredentialDto) { - Logger.log("Received data:", message, "AppController"); + this.logger.log(`Received data: ${message}`); return this.databaseIntegrationService.createCredential(message); } @@ -58,7 +64,7 @@ export class AppController { const token = message.token; const providerKey = message.providerKey; const data = await this.externalAuthIntegrationService.verifyToken(providerKey, token); - Logger.log(`Received data: ${JSON.stringify(data)}`, "AppController"); + this.logger.log(`Received data: ${JSON.stringify(data)}`); return data; } @@ -70,4 +76,20 @@ export class AppController { const data = await this.externalAuthIntegrationService.refreshToken(providerKey, refreshToken); return data; } + + @MessagePattern("basic_auth") + async handleBasicAuth(message: BasicAuthDto) { + // TODO: Add JWT + + const providerKey = message.providerKey; + return await this.externalAuthIntegrationService.authenticate(providerKey); + } + + @MessagePattern("basic_auth_verify") + async handleBasicAuthVerify(message: BasicAuthVerifyDto) { + // TODO: Add JWT + + const providerKey = message.providerKey; + return await this.externalAuthIntegrationService.verifyBasic(providerKey); + } } diff --git a/@core/cms/src/config/allowedCredentials.json b/@core/cms/src/config/allowedCredentials.json index ee51ae6..ca98b49 100644 --- a/@core/cms/src/config/allowedCredentials.json +++ b/@core/cms/src/config/allowedCredentials.json @@ -1,4 +1,15 @@ { - "Google": ["OAuth2", "APIKey"], - "Facebook": ["OAuth2"] -} + "Google": [ + "OAuth2", + "APIKey" + ], + "Facebook": [ + "OAuth2" + ], + "GitHub": [ + "OAuth2" + ], + "Mixpanel": [ + "Basic" + ] +} \ No newline at end of file diff --git a/@core/cms/src/dto/basic-auth-verify.dto.ts b/@core/cms/src/dto/basic-auth-verify.dto.ts new file mode 100644 index 0000000..785bdfd --- /dev/null +++ b/@core/cms/src/dto/basic-auth-verify.dto.ts @@ -0,0 +1,5 @@ +import { type BasicAuthString } from "~/external-auth-integration/auth-providers/basic-auth/@types"; + +export class BasicAuthVerifyDto { + providerKey: BasicAuthString; +} diff --git a/@core/cms/src/dto/basic-auth.dto.ts b/@core/cms/src/dto/basic-auth.dto.ts new file mode 100644 index 0000000..8e069b6 --- /dev/null +++ b/@core/cms/src/dto/basic-auth.dto.ts @@ -0,0 +1,5 @@ +import { type BasicAuthString } from "~/external-auth-integration/auth-providers/basic-auth/@types"; + +export class BasicAuthDto { + providerKey: BasicAuthString; +} diff --git a/@core/cms/src/external-auth-integration/@types/index.ts b/@core/cms/src/external-auth-integration/@types/index.ts new file mode 100644 index 0000000..01b8d5c --- /dev/null +++ b/@core/cms/src/external-auth-integration/@types/index.ts @@ -0,0 +1,7 @@ +import { type OAuthString } from "../auth-providers/oauth2/@types"; +import { type BasicAuthString } from "../auth-providers/basic-auth/@types"; +import { IOAuth } from "../auth-providers/oauth2/interface/ioauth.interface"; +import { IBasicAuth } from "../auth-providers/basic-auth/interface/basic-auth.interface"; + +export type AuthString = OAuthString | BasicAuthString; +export type AuthService = IOAuth | IBasicAuth; diff --git a/@core/cms/src/external-auth-integration/auth-callback.controller.ts b/@core/cms/src/external-auth-integration/auth-callback.controller.ts index 3dba720..d544a16 100644 --- a/@core/cms/src/external-auth-integration/auth-callback.controller.ts +++ b/@core/cms/src/external-auth-integration/auth-callback.controller.ts @@ -1,21 +1,14 @@ -import { - Controller, - Get, - Logger, - Query, - Res, - HttpException, - HttpStatus, -} from "@nestjs/common"; +import { Controller, Get, Logger, Query, Res, HttpException, HttpStatus } from "@nestjs/common"; import { AuthProviderFactory } from "./auth-provider.factory"; import { EncryptionDecryptionService } from "~/encryption-decryption/encryption-decryption.service"; import { decodeBase64UrlToString } from "./utils"; +import { type OAuthString } from "./auth-providers/oauth2/@types"; @Controller("oauth") export class AuthCallbackController { constructor( private providerFactory: AuthProviderFactory, - private encryptionDecryptionService: EncryptionDecryptionService + private encryptionDecryptionService: EncryptionDecryptionService, ) {} /** @@ -28,38 +21,27 @@ export class AuthCallbackController { async handleCallback(@Query() query: any, @Res() res: Response) { if (!query.state) { Logger.error("No state received", "AuthCallbackController"); - throw new HttpException( - "State parameter is missing", - HttpStatus.BAD_REQUEST - ); + throw new HttpException("State parameter is missing", HttpStatus.BAD_REQUEST); } try { const encryptedState = decodeBase64UrlToString(query.state); - const decryptedState = - await this.encryptionDecryptionService.decryptData(encryptedState); + const decryptedState = await this.encryptionDecryptionService.decryptData(encryptedState); const state = JSON.parse(decryptedState); - if ( - !state.providerInfo || - !state.providerInfo.provider || - !state.providerInfo.version - ) { + if (!state.providerInfo || !state.providerInfo.provider || !state.providerInfo.version) { throw new Error("Invalid state format"); } // TODO: Verify user JWT - const key = `oauth-${state.providerInfo.provider}-${state.providerInfo.version}`; - Logger.log(`Calling oauth callback for provider ${state.providerInfo.provider} ${state.providerInfo.version}`) + const key: OAuthString = `oauth-${state.providerInfo.provider}-${state.providerInfo.version}`; + Logger.log(`Calling oauth callback for provider ${state.providerInfo.provider} ${state.providerInfo.version}`); const providerService = this.providerFactory.getProvider(key); await providerService.handleCallback(query, res); } catch (error) { Logger.error(error.message, "AuthCallbackController"); - throw new HttpException( - "Error processing the OAuth callback", - HttpStatus.INTERNAL_SERVER_ERROR - ); + throw new HttpException("Error processing the OAuth callback", HttpStatus.INTERNAL_SERVER_ERROR); } } } diff --git a/@core/cms/src/external-auth-integration/auth-provider.factory.ts b/@core/cms/src/external-auth-integration/auth-provider.factory.ts index 97ffe9a..0033b7d 100644 --- a/@core/cms/src/external-auth-integration/auth-provider.factory.ts +++ b/@core/cms/src/external-auth-integration/auth-provider.factory.ts @@ -1,34 +1,55 @@ import { Injectable } from "@nestjs/common"; -import { IOAuth } from "./auth-providers/oauth2/interface/ioauth.interface"; +import type { AuthString, AuthService } from "./@types"; import { GoogleV2OAuth2Service } from "./auth-providers/oauth2/google/v2/google.v2.service"; import { GithubV1OAuth2Service } from "./auth-providers/oauth2/github/v1/github.v1.service"; import { AirtableV1OAuth2Service } from "./auth-providers/oauth2/airtable/v1/airtable.v1.service"; import { SlackV2OAuth2Service } from "./auth-providers/oauth2/slack/v2/slack.v2.service"; +import { MixpanelV1BasicAuthService } from "./auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.service"; +import { type OAuthString } from "./auth-providers/oauth2/@types"; +import { IOAuth } from "./auth-providers/oauth2/interface/ioauth.interface"; +import { type BasicAuthString } from "./auth-providers/basic-auth/@types"; +import { IBasicAuth } from "./auth-providers/basic-auth/interface/basic-auth.interface"; @Injectable() export class AuthProviderFactory { - private readonly providerMap = new Map(); + // A map to hold the associations between authentication keys and their corresponding services + private readonly providerMap = new Map(); constructor( private googleV2OAuth2Service: GoogleV2OAuth2Service, private githubV1OAuth2Service: GithubV1OAuth2Service, private airtableV1OAuth2Service: AirtableV1OAuth2Service, private slackV2OAuth2Service: SlackV2OAuth2Service, + private mixpanelV1BasicAuthService: MixpanelV1BasicAuthService, ) { + // Register available authentication providers this.registerProvider("oauth-google-v2", this.googleV2OAuth2Service); this.registerProvider("oauth-github-v1", this.githubV1OAuth2Service); this.registerProvider("oauth-airtable-v1", this.airtableV1OAuth2Service); this.registerProvider("oauth-slack-v2", this.slackV2OAuth2Service); + + this.registerProvider("basic-mixpanel-v1", this.mixpanelV1BasicAuthService); } - private registerProvider(key: string, provider: IOAuth) { + // Function overloading for registerProvider to handle different types of auth providers + private registerProvider(key: OAuthString, provider: IOAuth): void; + private registerProvider(key: BasicAuthString, provider: IBasicAuth): void; + private registerProvider(key: AuthString, provider: AuthService): void { + // Register a provider with a specific key in the map this.providerMap.set(key, provider); } - public getProvider(key: string): IOAuth { + // Function overloading for getProvider to retrieve the appropriate type of auth provider + public getProvider(key: OAuthString): IOAuth; + public getProvider(key: BasicAuthString): IBasicAuth; + public getProvider(key: AuthString): AuthService; + + public getProvider(key: AuthString): AuthService { + // Retrieve the provider associated with the given key const provider = this.providerMap.get(key); if (!provider) { - throw new Error(`Provider not found: ${key}`); + // If the provider is not found, throw an error + throw new Error(`Authentication provider not found for key: ${key}`); } return provider; } diff --git a/@core/cms/src/external-auth-integration/auth-providers/basic-auth/@types/index.ts b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/@types/index.ts new file mode 100644 index 0000000..c2e1447 --- /dev/null +++ b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/@types/index.ts @@ -0,0 +1,17 @@ +export type BasicAuthString = `basic-${string}`; + +export interface BasicAuthProvider { + authenticateUrl: string; + verifyUrl?: string; +} + +export interface BasicAuthConfig { + provider: BasicAuthProvider; +} + +export interface BasicAuthVerificationResponse { + isValid: boolean; + expiresIn?: number; + // Other fields as per your application's requirements + [key: string]: any; +} diff --git a/@core/cms/src/external-auth-integration/auth-providers/basic-auth/basic-auth.module.ts b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/basic-auth.module.ts new file mode 100644 index 0000000..8df0549 --- /dev/null +++ b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/basic-auth.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { HttpModule } from "@nestjs/axios"; +import { EncryptionDecryptionModule } from "~/encryption-decryption/encryption-decryption.module"; +import { MixpanelV1BasicAuthService } from "./mixpanel/v1/mixpanel.v1.service"; +import { MixpanelV1BasicAuthConfig } from "./mixpanel/v1/mixpanel.v1.config"; + +@Module({ + imports: [HttpModule, EncryptionDecryptionModule], + providers: [ + MixpanelV1BasicAuthService, + { + provide: "MixpanelV1BasicAuthConfig", + useFactory: () => new MixpanelV1BasicAuthConfig().basicAuthConfig, + }, + ], + exports: [MixpanelV1BasicAuthService], +}) +export class BasicAuthModule {} diff --git a/@core/cms/src/external-auth-integration/auth-providers/basic-auth/interface/basic-auth.interface.ts b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/interface/basic-auth.interface.ts new file mode 100644 index 0000000..7ec8f68 --- /dev/null +++ b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/interface/basic-auth.interface.ts @@ -0,0 +1,5 @@ +export interface IBasicAuth { + authenticate(username: string, password: string): Promise; + + verify?(username: string, password: string): Promise; +} diff --git a/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.config.ts b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.config.ts new file mode 100644 index 0000000..f905378 --- /dev/null +++ b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.config.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common"; +import { BasicAuthConfig } from "../../@types"; + +/** + * Service for configuring Mixpanel V1 Basic HTTP Authentication. + */ +@Injectable() +export class MixpanelV1BasicAuthConfig { + public basicAuthConfig: BasicAuthConfig; + + constructor() { + this.basicAuthConfig = { + provider: { + authenticateUrl: "https://mixpanel.com/api/app/me", + verifyUrl: "https://mixpanel.com/api/app/organizations/{organizationId}/service-accounts", + }, + }; + } +} diff --git a/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.service.ts b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.service.ts new file mode 100644 index 0000000..909dc28 --- /dev/null +++ b/@core/cms/src/external-auth-integration/auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.service.ts @@ -0,0 +1,67 @@ +import { Injectable, Inject, Logger } from "@nestjs/common"; +import { HttpService } from "@nestjs/axios"; +import { EncryptionDecryptionService } from "~/encryption-decryption/encryption-decryption.service"; +import { lastValueFrom } from "rxjs"; +import { map } from "rxjs/operators"; + +import { IBasicAuth } from "../../interface/basic-auth.interface"; +import { BasicAuthConfig, BasicAuthVerificationResponse } from "../../@types"; + +/** + * Service to handle Mixpanel V1 Basic HTTP Authentication. + */ +@Injectable() +export class MixpanelV1BasicAuthService implements IBasicAuth { + constructor( + @Inject("MixpanelV1BasicAuthConfig") private readonly config: BasicAuthConfig, + private readonly httpService: HttpService, + private readonly encryptionDecryptionService: EncryptionDecryptionService, + ) {} + + private readonly logger = new Logger(MixpanelV1BasicAuthService.name); + + async authenticate(username: string, password: string): Promise { + try { + const { authenticateUrl } = this.config.provider; + const data = await lastValueFrom( + this.httpService.get(authenticateUrl, { auth: { username, password } }).pipe(map((resp) => resp.data)), + ); + + return { + status: data.status, + organizationId: Object.keys(data.results?.organizations).at(0), + }; + } catch (error) { + this.logger.error(`Authentication failed: ${error}`); + const data = error.response?.data; + return data; + } + } + + async verify(username: string, password: string): Promise { + try { + const authenticationResponse = await this.authenticate(username, password); + if (!authenticationResponse["organizationId"]) { + throw new Error("Authentication failed"); + } + + let { verifyUrl } = this.config.provider; + if (!verifyUrl) { + throw new Error("verifyUrl not found"); + } + + verifyUrl = verifyUrl.replace("{organizationId}", authenticationResponse["organizationId"]); + const data = await lastValueFrom( + this.httpService.get(verifyUrl, { auth: { username, password } }).pipe(map((resp) => resp.data)), + ); + + return { + isValid: true, + expiresIn: data.results?.expires, + }; + } catch (error) { + this.logger.error(`Service account verification failed: ${error}`); + return { isValid: false }; + } + } +} diff --git a/@core/cms/src/external-auth-integration/auth-providers/oauth2/@types/index.ts b/@core/cms/src/external-auth-integration/auth-providers/oauth2/@types/index.ts index 2c36c45..b47572a 100644 --- a/@core/cms/src/external-auth-integration/auth-providers/oauth2/@types/index.ts +++ b/@core/cms/src/external-auth-integration/auth-providers/oauth2/@types/index.ts @@ -1,3 +1,5 @@ +export type OAuthString = `oauth-${string}`; + /** * Represents the client credentials for OAuth2 authentication. * @export diff --git a/@core/cms/src/external-auth-integration/auth-providers/oauth2/github/v1/github.v1.service.ts b/@core/cms/src/external-auth-integration/auth-providers/oauth2/github/v1/github.v1.service.ts index 362e655..9b65c4b 100644 --- a/@core/cms/src/external-auth-integration/auth-providers/oauth2/github/v1/github.v1.service.ts +++ b/@core/cms/src/external-auth-integration/auth-providers/oauth2/github/v1/github.v1.service.ts @@ -47,8 +47,14 @@ export class GithubV1OAuth2Service implements IOAuth { } const tokenResponse = await exchangeCodeForToken(this.httpService, this.config, query.code); - Logger.log("Token response", tokenResponse); - res.status(HttpStatus.OK).json(tokenResponse); + + const params = new URLSearchParams(tokenResponse); + const data = {}; + for (const [key, value] of params) { + data[key] = value; + } + + res.status(HttpStatus.OK).json(data); } catch (error) { Logger.error("Error exchanging code for token", error); throw new HttpException("Error exchanging code for token", HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/@core/cms/src/external-auth-integration/external-auth-integration.module.ts b/@core/cms/src/external-auth-integration/external-auth-integration.module.ts index 6d9ae00..e48db01 100644 --- a/@core/cms/src/external-auth-integration/external-auth-integration.module.ts +++ b/@core/cms/src/external-auth-integration/external-auth-integration.module.ts @@ -3,10 +3,11 @@ import { EncryptionDecryptionModule } from "~/encryption-decryption/encryption-d import { AuthProviderFactory } from "./auth-provider.factory"; import { AuthCallbackController } from "./auth-callback.controller"; import { OAuth2Module } from "./auth-providers/oauth2/oauth2.module"; +import { BasicAuthModule } from "./auth-providers/basic-auth/basic-auth.module"; import { ExternalAuthIntegrationService } from "./external-auth-integration.service"; @Module({ - imports: [OAuth2Module, EncryptionDecryptionModule], + imports: [OAuth2Module, BasicAuthModule, EncryptionDecryptionModule], providers: [AuthProviderFactory, ExternalAuthIntegrationService], controllers: [AuthCallbackController], exports: [ExternalAuthIntegrationService], diff --git a/@core/cms/src/external-auth-integration/external-auth-integration.service.ts b/@core/cms/src/external-auth-integration/external-auth-integration.service.ts index 1114514..8740fff 100644 --- a/@core/cms/src/external-auth-integration/external-auth-integration.service.ts +++ b/@core/cms/src/external-auth-integration/external-auth-integration.service.ts @@ -1,40 +1,46 @@ import { Injectable } from "@nestjs/common"; import { AuthProviderFactory } from "./auth-provider.factory"; +import type { AuthString } from "./@types"; +import type { OAuthString } from "./auth-providers/oauth2/@types"; +import type { BasicAuthString } from "./auth-providers/basic-auth/@types"; @Injectable() export class ExternalAuthIntegrationService { constructor(private readonly authProviderFactory: AuthProviderFactory) {} - async authenticate(providerKey: string): Promise { - const authProvider = this.authProviderFactory.getProvider(providerKey); - if (!authProvider) { - throw new Error( - `Authentication provider not found for key: ${providerKey}` - ); + async authenticate(providerKey: AuthString): Promise { + try { + const authProvider = this.authProviderFactory.getProvider(providerKey); + return await authProvider.authenticate(); + } catch (error) { + return { error: error.message }; } - - return await authProvider.authenticate(); } - async verifyToken(providerKey: string, token: string): Promise { + async verifyToken(providerKey: OAuthString, token: string): Promise { const authProvider = this.authProviderFactory.getProvider(providerKey); if (!authProvider) { - throw new Error( - `Authentication provider not found for key: ${providerKey}` - ); + throw new Error(`Authentication provider not found for key: ${providerKey}`); } return await authProvider.verifyToken(token); } - async refreshToken(providerKey: string, refreshToken: string): Promise { + async refreshToken(providerKey: OAuthString, refreshToken: string): Promise { const authProvider = this.authProviderFactory.getProvider(providerKey); if (!authProvider) { - throw new Error( - `Authentication provider not found for key: ${providerKey}` - ); + throw new Error(`Authentication provider not found for key: ${providerKey}`); } return await authProvider.refreshToken(refreshToken); } + + async verifyBasic(providerKey: BasicAuthString): Promise { + try { + const authProvider = this.authProviderFactory.getProvider(providerKey); + return await authProvider.verify(); + } catch (error) { + return { error: error.message }; + } + } }