Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reporting data issues #2637

Merged
merged 15 commits into from
Oct 7, 2024
Merged
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
5 changes: 5 additions & 0 deletions backend/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
"webhookSecret": "CROWD_GITHUB_WEBHOOK_SECRET",
"isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED"
},
"githubIssueReporter": {
"appId": "CROWD_GITHUB_ISSUE_REPORTER_APP_ID",
"privateKey": "CROWD_GITHUB_ISSUE_REPORTER_PRIVATE_KEY",
"installationId": "CROWD_GITHUB_ISSUE_REPORTER_INSTALLATION_ID"
},
"stackexchange": {
"key": "CROWD_STACKEXCHANGE_KEY"
},
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@crowd/types": "workspace:*",
"@google-cloud/storage": "5.3.0",
"@octokit/auth-app": "^3.6.1",
"@octokit/core": "^6.1.2",
"@octokit/graphql": "^4.8.0",
"@octokit/request": "^5.6.3",
"@opensearch-project/opensearch": "^2.11.0",
Expand Down
5 changes: 5 additions & 0 deletions backend/src/api/member/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ export default (app) => {
require('./organization').default(app)
require('./attributes').default(app)
require('./affiliation').default(app)

app.post(
`/tenant/:tenantId/member/:id/data-issue`,
safeWrap(require('./memberDataIssueCreate').default),
)
}
16 changes: 16 additions & 0 deletions backend/src/api/member/memberDataIssueCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DataIssueEntity } from '@crowd/types'

import PermissionChecker from '../../services/user/permissionChecker'
import Permissions from '../../security/permissions'
import DataIssueService from '@/services/dataIssueService'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.dataIssueCreate)

const payload = await new DataIssueService(req).createDataIssue(
{ ...req.body, entity: DataIssueEntity.PERSON },
req.params.id,
)

await req.responseHandler.success(req, res, payload)
}
5 changes: 5 additions & 0 deletions backend/src/api/organization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ export default (app) => {

// list organizations across all segments
app.post(`/tenant/:tenantId/organization/list`, safeWrap(require('./organizationList').default))

app.post(
`/tenant/:tenantId/organization/:id/data-issue`,
safeWrap(require('./organizationDataIssueCreate').default),
)
}
16 changes: 16 additions & 0 deletions backend/src/api/organization/organizationDataIssueCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DataIssueEntity } from '@crowd/types'

import PermissionChecker from '../../services/user/permissionChecker'
import Permissions from '../../security/permissions'
import DataIssueService from '@/services/dataIssueService'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.dataIssueCreate)

const payload = await new DataIssueService(req).createDataIssue(
{ ...req.body, entity: DataIssueEntity.ORGANIZATION },
req.params.id,
)

await req.responseHandler.success(req, res, payload)
}
4 changes: 4 additions & 0 deletions backend/src/conf/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import config from 'config'
import { IRedisConfiguration } from '@crowd/redis'
import { ISearchSyncApiConfig } from '@crowd/opensearch'
import { IGithubIssueReporterConfiguration } from '@crowd/types'
import { IDatabaseConfig } from '@crowd/data-access-layer/src/database'
import { IQueueClientConfig } from '@crowd/queue'
import {
Expand Down Expand Up @@ -113,6 +114,9 @@ export const DISCORD_CONFIG: DiscordConfiguration = config.get<DiscordConfigurat

export const GITHUB_CONFIG: GithubConfiguration = config.get<GithubConfiguration>('github')

export const GITHUB_ISSUE_REPORTER_CONFIG: IGithubIssueReporterConfiguration =
config.get<IGithubIssueReporterConfiguration>('githubIssueReporter')

export const SENDGRID_CONFIG: SendgridConfiguration = config.get<SendgridConfiguration>('sendgrid')

export const NETLIFY_CONFIG: NetlifyConfiguration = config.get<NetlifyConfiguration>('netlify')
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
create table public."dataIssues" (
"id" uuid,
entity text not null,
"profileUrl" text not null,
"dataIssue" text not null,
"dataType" text not null,
"githubIssueUrl" text not null,
"description" text not null,
"createdById" uuid not null,
"createdAt" timestamp with time zone default now() not null,
"updatedAt" timestamp with time zone default now() not null,
"resolutionEmailSentAt" timestamp with time zone default null,
"resolutionEmailSentTo" text null,
primary key ("id"),
foreign key ("createdById") references users (id) on delete cascade,
unique ("githubIssueUrl")
);
11 changes: 11 additions & 0 deletions backend/src/security/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,17 @@ class Permissions {
TenantPlans.Scale,
],
},
dataIssueCreate: {
id: 'dataIssueCreate',
allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly],
allowedPlans: [
TenantPlans.Essential,
TenantPlans.Growth,
TenantPlans.EagleEye,
TenantPlans.Enterprise,
TenantPlans.Scale,
],
},
}
}

Expand Down
119 changes: 119 additions & 0 deletions backend/src/services/dataIssueService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Octokit } from '@octokit/core'
import { request } from '@octokit/request'
import { createAppAuth } from '@octokit/auth-app'
import { LoggerBase } from '@crowd/logging'
import { PgPromiseQueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
import { createDataIssue } from '@crowd/data-access-layer/src/data_issues'
import { findOrgById, OrganizationField } from '@crowd/data-access-layer/src/orgs'
import { findMemberById, MemberField } from '@crowd/data-access-layer/src/members'
import { DataIssueEntity } from '@crowd/types'
import { InstallationAccessTokenData } from '@octokit/auth-app/dist-types/types'
import { IServiceOptions } from './IServiceOptions'
import SequelizeRepository from '@/database/repositories/sequelizeRepository'
import { GITHUB_ISSUE_REPORTER_CONFIG } from '@/conf'

export interface IDataIssueCreatePayload {
entity: DataIssueEntity
profileUrl: string
dataIssue: string
dataType: string
description: string
githubIssueUrl: string
createdById: string
}

export default class DataIssueService extends LoggerBase {
private readonly qx: PgPromiseQueryExecutor

private readonly DATA_ISSUES_GITHUB_REPO: string = 'linux-foundation-support'

private readonly DATA_ISSUES_GITHUB_OWNER: string = 'CrowdDotDev'

options: IServiceOptions

constructor(options: IServiceOptions) {
super(options.log)
this.options = options
}

public async createDataIssue(data: IDataIssueCreatePayload, entityId: string) {
const qx = SequelizeRepository.getQueryExecutor(this.options)
const user = SequelizeRepository.getCurrentUser(this.options)

let entityName: string
let reportedBy: string

if (data.entity === DataIssueEntity.ORGANIZATION) {
const organization = await findOrgById(qx, entityId, [
OrganizationField.ID,
OrganizationField.DISPLAY_NAME,
])
entityName = organization.displayName
} else if (data.entity === DataIssueEntity.PERSON) {
const member = await findMemberById(qx, entityId, [MemberField.ID, MemberField.DISPLAY_NAME])
entityName = member.displayName
} else {
throw new Error(`Unsupported data issue entity ${data.entity}!1`)
}

if (user.fullName) {
reportedBy = `${user.fullName} - ${user.email}`
} else {
reportedBy = `${user.email}`
}

const appToken = await DataIssueService.getGitHubAppToken(
GITHUB_ISSUE_REPORTER_CONFIG.appId,
GITHUB_ISSUE_REPORTER_CONFIG.privateKey,
GITHUB_ISSUE_REPORTER_CONFIG.installationId,
)

try {
const result = await request(
`POST /repos/${this.DATA_ISSUES_GITHUB_OWNER}/${this.DATA_ISSUES_GITHUB_REPO}/issues`,
{
headers: {
authorization: `token ${appToken}`,
},
title: `[Data Issue] ${entityName} (${data.entity[0].toUpperCase()}${data.entity
.slice(1)
.toLowerCase()})`,
body: `**Entity**\n${entityName}\n\n**Profile**\n[${data.profileUrl}](${data.profileUrl})\n\n**Data Issue**\n${data.dataIssue}\n\n**Description**\n${data.description}\n\n**Reported by**\n${reportedBy}`,
labels: ['Data issue'],
},
)
const res = await createDataIssue(qx, {
...data,
githubIssueUrl: result.data.html_url,
createdById: user.id,
})

return res
} catch (error) {
this.log.info(error)
throw new Error('Error during session create!')
}
}

public static async getGitHubAppToken(
appId: string,
privateKey: string,
installationId: string,
): Promise<string> {
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
privateKey,
installationId,
},
})

const authResponse = await octokit.auth({
type: 'installation',
installationId,
})

return (authResponse as InstallationAccessTokenData).token
}
}
2 changes: 1 addition & 1 deletion frontend/config/styles/components/radio.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
--lf-radio-unchecked-background: var(--lf-color-white);

// Checked
--lf-radio-checked-border: var(--lf-color-gray-900);
--lf-radio-checked-border: var(--lf-color-primary-500);
--lf-radio-checked-background: var(--lf-color-white);

// Disabled Unchecked
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<div class="sm:block md:hidden lg:hidden xl:hidden">
<app-resize-page />
</div>
<lf-globals />
</div>
</template>

Expand All @@ -27,11 +28,13 @@ import { useActivityTypeStore } from '@/modules/activity/store/type';
import { useAuthStore } from '@/modules/auth/store/auth.store';
import useSessionTracking from '@/shared/modules/monitoring/useSessionTracking';
import { useLfSegmentsStore } from '@/modules/lf/segments/store';
import LfGlobals from '@/shared/components/globals.vue';

export default {
name: 'App',

components: {
LfGlobals,
AppResizePage,
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
<div class="flex">
<lf-button-group>
<!-- Merge suggestions -->
<lf-button
v-if="!hasPermission(LfPermission.mergeMembers)"
type="secondary"
@click="setReportDataModal({
contributor: props.contributor,
})"
>
<lf-icon name="feedback-line" class="text-red-500" /> Report data issue
</lf-button>
<lf-button
v-if="!isMasked(props.contributor) && mergeSuggestionsCount > 0 && hasPermission(LfPermission.mergeMembers)"
type="secondary"
Expand Down Expand Up @@ -86,6 +95,7 @@ import LfContributorDropdown from '@/modules/contributor/components/shared/contr
import { ContributorApiService } from '@/modules/contributor/services/contributor.api.service';
import { Contributor } from '@/modules/contributor/types/Contributor';
import useContributorHelpers from '@/modules/contributor/helpers/contributor.helpers';
import { useSharedStore } from '@/shared/pinia/shared.store';

const props = defineProps<{
contributor: Contributor,
Expand All @@ -96,6 +106,7 @@ const emit = defineEmits<{(e: 'reload'): any}>();
const { hasPermission } = usePermissions();

const { isMasked } = useContributorHelpers();
const { setReportDataModal } = useSharedStore();

const isMergeSuggestionsDialogOpen = ref<boolean>(false);
const isMergeDialogOpen = ref<Contributor | null>(null);
Expand Down
Loading
Loading