Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22,177 changes: 22,177 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions src/core/application/use-cases/auth/checkSsoUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules medium

import {

The file check-sso.use-case.ts is named using kebab-case, which violates the rule requiring camelCase for new files. Please rename the file to follow the camelCase convention (e.g., checkSsoUseCase.ts).

Kody Rule violation: Seguir padrão de nomenclatura de arquivos

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

IOrganizationParametersService,
ORGANIZATION_PARAMETERS_SERVICE_TOKEN,
} from '@/core/domain/organizationParameters/contracts/organizationParameters.service.contract';
import { SSOConfig } from '@/core/domain/organizationParameters/types/sso-config.type';
import { PinoLoggerService } from '@/core/infrastructure/adapters/services/logger/pino.service';
import { OrganizationParametersKey } from '@/shared/domain/enums/organization-parameters-key.enum';
import { IUseCase } from '@/shared/domain/interfaces/use-case.interface';
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class CheckSSOUseCase implements IUseCase {
constructor(
@Inject(ORGANIZATION_PARAMETERS_SERVICE_TOKEN)
private readonly organizationParametersService: IOrganizationParametersService,
private readonly logger: PinoLoggerService,
) { }

public async execute(email: string): Promise<{
ssoEnabled: boolean;
redirectUrl?: string;
organizationId?: string;
}> {
try {
if (!email) {
return { ssoEnabled: false };
}

const domain = email.split('@')[1];
if (!domain) {
return { ssoEnabled: false };
}

const ssoConfigs =
await this.organizationParametersService.findByKeyAndValue({
configKey: OrganizationParametersKey.SSO_CONFIG,
configValue: { enabled: true },
fuzzy: true,
});

if (!ssoConfigs || ssoConfigs.length === 0) {
return { ssoEnabled: false };
}

const lowercaseDomain = domain.toLowerCase();
const matchingConfig = ssoConfigs.find((param) => {
const config = param.configValue as SSOConfig;
return config?.domains?.some(
(d) => d.toLowerCase() === lowercaseDomain,
);
});

if (!matchingConfig) {
return { ssoEnabled: false };
}

const config = matchingConfig.configValue as SSOConfig;
const redirectUri = config.redirectUris[0]; // Assuming first one for now

// Construct Google OIDC URL
const scope = encodeURIComponent('openid email profile');
const responseType = 'code';
const clientId = encodeURIComponent(config.clientId);
const redirect = encodeURIComponent(redirectUri);
const state = encodeURIComponent(
JSON.stringify({
organizationId: matchingConfig.organization.uuid,
provider: 'google',
}),
);

const googleAuthUrl = `${config.issuer}/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirect}&response_type=${responseType}&scope=${scope}&state=${state}&access_type=offline&prompt=consent`;

return {
ssoEnabled: true,
redirectUrl: googleAuthUrl,
organizationId: matchingConfig.organization.uuid,
};
} catch (error) {
this.logger.error({
message: 'Error checking SSO status',
error,
context: CheckSSOUseCase.name,
metadata: { email },
});
return { ssoEnabled: false };
}
}
}
188 changes: 188 additions & 0 deletions src/core/application/use-cases/auth/ssoLoginUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules medium

// Rename file to: ssoLoginUseCase.ts
import {

The file name 'sso-login.use-case.ts' uses kebab-case, which violates the rule requiring camelCase for new files. Please rename the file to 'ssoLoginUseCase.ts'.

Kody Rule violation: Seguir padrão de nomenclatura de arquivos

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

AUTH_SERVICE_TOKEN,
IAuthService,
} from '@/core/domain/auth/contracts/auth.service.contracts';
import {
IOrganizationService,
ORGANIZATION_SERVICE_TOKEN,
} from '@/core/domain/organization/contracts/organization.service.contract';
import {
ITeamService,
TEAM_SERVICE_TOKEN,
} from '@/core/domain/team/contracts/team.service.contract';
import {
ITeamMemberService,
TEAM_MEMBERS_SERVICE_TOKEN,
} from '@/core/domain/teamMembers/contracts/teamMembers.service.contracts';
import { TeamMemberRole } from '@/core/domain/teamMembers/enums/teamMemberRole.enum';
import {
IUsersService,
USER_SERVICE_TOKEN,
} from '@/core/domain/user/contracts/user.service.contract';
import { IUser } from '@/core/domain/user/interfaces/user.interface';
import { PinoLoggerService } from '@/core/infrastructure/adapters/services/logger/pino.service';
import { AuthProvider } from '@/shared/domain/enums/auth-provider.enum';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import axios from 'axios';
import { randomBytes } from 'node:crypto';
import { GetOrganizationsByDomainUseCase } from '@/core/application/use-cases/organization/get-organizations-domain.use-case';
import { SignUpUseCase } from './signup.use-case';
import { SSOConfig } from '@/core/domain/organizationParameters/types/ssoConfig.type';
import {
IOrganizationParametersService,
ORGANIZATION_PARAMETERS_SERVICE_TOKEN,
} from '@/core/domain/organizationParameters/contracts/organizationParameters.service.contract';
import { OrganizationParametersKey } from '@/shared/domain/enums/organization-parameters-key.enum';

@Injectable()
export class SSOLoginUseCase {
constructor(
@Inject(AUTH_SERVICE_TOKEN)
private readonly authService: IAuthService,
@Inject(USER_SERVICE_TOKEN)
private readonly usersService: IUsersService,
@Inject(ORGANIZATION_SERVICE_TOKEN)
private readonly organizationService: IOrganizationService,
@Inject(TEAM_MEMBERS_SERVICE_TOKEN)
private readonly teamMembersService: ITeamMemberService,
@Inject(TEAM_SERVICE_TOKEN)
private readonly teamService: ITeamService,
@Inject(ORGANIZATION_PARAMETERS_SERVICE_TOKEN)
private readonly organizationParametersService: IOrganizationParametersService,
private readonly signUpUseCase: SignUpUseCase,
private readonly getOrganizationsByDomainUseCase: GetOrganizationsByDomainUseCase,
private readonly logger: PinoLoggerService,
) { }

public async execute(
code: string,
state: string,
): Promise<{ accessToken: string; refreshToken: string }> {
let organizationId: string | undefined;
try {
const stateJson = JSON.parse(decodeURIComponent(state));
organizationId = stateJson.organizationId;

if (!organizationId) {
throw new UnauthorizedException('Invalid state: missing organizationId');
}

// 1. Get SSO Config for the organization
const orgParams = await this.organizationParametersService.findByKeyAndValue({
configKey: OrganizationParametersKey.SSO_CONFIG,
configValue: { enabled: true },
fuzzy: true,
});

const orgConfig = orgParams.find(
(p) => p.organization.uuid === organizationId,
);

if (!orgConfig) {
throw new UnauthorizedException('SSO not enabled for this organization');
}

const ssoConfig = orgConfig.configValue as SSOConfig;

// 2. Exchange code for tokens
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: ssoConfig.clientId,
client_secret: ssoConfig.clientSecret,
redirect_uri: ssoConfig.redirectUris[0],
grant_type: 'authorization_code',
},
);

const { id_token, refresh_token } = tokenResponse.data;

// 3. Verify ID Token and get email (Simplified: just decode for now, in prod verify signature)
// Using google's tokeninfo endpoint for verification is safer
const tokenInfo = await axios.get(
`https://oauth2.googleapis.com/tokeninfo?id_token=${id_token}`,
);

const email = tokenInfo.data.email;
const name = tokenInfo.data.name || email.split('@')[0];

if (!email) {
throw new UnauthorizedException('Could not retrieve email from ID token');
}

// 4. Find or Create User
let user = await this.authService.validateUser({ email });

if (!user || !user.uuid) {

Check warning on line 117 in src/core/application/use-cases/auth/ssoLoginUseCase.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=kodustech_kodus-ai&issues=AZrgV52KwSXZKUwYcZKW&open=AZrgV52KwSXZKUwYcZKW&pullRequest=416
// Create user and add to the SSO organization
user = await this.signUpUseCase.execute({
email,
name,
password: randomBytes(32).toString('base64').slice(0, 32),
organizationId,
});
} else {
// Ensure user is member of the SSO organization
await this.ensureOrganizationMembership(user as IUser, organizationId);
}

// 5. Auto Join Logic (for OTHER organizations)
const domain = email.split('@')[1];
const autoJoinOrgs = await this.getOrganizationsByDomainUseCase.execute(domain);

for (const org of autoJoinOrgs) {
if (org.uuid !== organizationId) {
await this.ensureOrganizationMembership(user as IUser, org.uuid);
}
}

// 6. Login
const tokens = await this.authService.login(
user as IUser,
AuthProvider.GOOGLE,
{ refreshToken: refresh_token },
);

return tokens;
} catch (error) {
this.logger.error({
message: 'SSO Login failed',
error,
context: SSOLoginUseCase.name,
metadata: {
organizationId,
},
});
throw new UnauthorizedException('SSO Login failed');
}
}

private async ensureOrganizationMembership(user: IUser, organizationId: string) {
const member = await this.teamMembersService.findOne({
user: { uuid: user.uuid },
organization: { uuid: organizationId },
});

if (!member) {
const organization = await this.organizationService.findOne({
uuid: organizationId,
});

const team = await this.teamService.findOne({
organization: { uuid: organizationId },
});

if (organization && team) {
await this.teamMembersService.create({
user,
name: user.email,
organization,
team,
teamRole: TeamMemberRole.MEMBER,
status: true,
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,13 @@ export interface ICommentManagerService {
pullRequestMessagesConfig?: IPullRequestMessages,
dryRun?: CodeReviewPipelineContext['dryRun'],
): Promise<void>;

createNoChangesComment(
organizationAndTeamData: OrganizationAndTeamData,
prNumber: number,
repository: { name: string; id: string },
platformType: PlatformType,
language: string,
dryRun?: CodeReviewPipelineContext['dryRun'],
): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface SSOConfig {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules medium

export interface SSOConfig {

The file name sso-config.type.ts violates the Kody Rule requiring camelCase for new files. Please rename the file to ssoConfig.type.ts.

Kody Rule violation: Seguir padrão de nomenclatura de arquivos

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

enabled: boolean;
issuer: string;
clientId: string;
clientSecret: string;
redirectUris: string[];
domains: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
IPullRequestManagerService,
PULL_REQUEST_MANAGER_SERVICE_TOKEN,
} from '@/core/domain/codeBase/contracts/PullRequestManagerService.contract';
import {
COMMENT_MANAGER_SERVICE_TOKEN,
ICommentManagerService,
} from '@/core/domain/codeBase/contracts/CommentManagerService.contract';
import { CodeReviewPipelineContext } from '../context/code-review-pipeline.context';
import { PinoLoggerService } from '../../../logger/pino.service';
import {
Expand All @@ -25,6 +29,8 @@
constructor(
@Inject(PULL_REQUEST_MANAGER_SERVICE_TOKEN)
private pullRequestHandlerService: IPullRequestManagerService,
@Inject(COMMENT_MANAGER_SERVICE_TOKEN)
private commentManagerService: ICommentManagerService,

Check warning on line 33 in src/core/infrastructure/adapters/services/codeBase/codeReviewPipeline/stages/fetch-changed-files.stage.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'commentManagerService: ICommentManagerService' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=kodustech_kodus-ai&issues=AZrgV5w6wSXZKUwYcZKV&open=AZrgV5w6wSXZKUwYcZKV&pullRequest=416
private logger: PinoLoggerService,
) {
super();
Expand Down Expand Up @@ -64,6 +70,17 @@
? AutomationMessage.NO_FILES_AFTER_IGNORE
: AutomationMessage.TOO_MANY_FILES;

if (!files?.length && context.lastExecution) {
await this.commentManagerService.createNoChangesComment(
context.organizationAndTeamData,
context.pullRequest.number,
context.repository,
context.platformType,
context.codeReviewConfig?.languageResultPrompt ?? 'en-US',
context.dryRun,
);
}

this.logger.warn({
message: `Skipping code review for PR#${context.pullRequest.number} - ${msg}`,
context: FetchChangedFilesStage.name,
Expand Down
Loading