From 10eed447151bebcf5a76196f283f9d4a162f6fc2 Mon Sep 17 00:00:00 2001 From: anilb Date: Tue, 24 Sep 2024 14:30:27 +0200 Subject: [PATCH 1/9] org & member data reporting with github issues and db save --- .../config/custom-environment-variables.json | 5 + backend/package.json | 1 + backend/src/api/member/index.ts | 5 + .../src/api/member/memberDataIssueCreate.ts | 16 +++ backend/src/api/organization/index.ts | 5 + .../organizationDataIssueCreate.ts | 16 +++ backend/src/conf/configTypes.ts | 5 + backend/src/conf/index.ts | 4 + .../U1727097932__createDataIssuesTable.sql | 0 .../V1727097932__createDataIssuesTable.sql | 14 ++ backend/src/security/permissions.ts | 11 ++ backend/src/services/dataIssueService.ts | 123 ++++++++++++++++++ pnpm-lock.yaml | 83 ++++++++++++ .../src/data_issues/index.ts | 60 +++++++++ services/libs/types/src/dataIssues.ts | 14 ++ services/libs/types/src/enums/dataIssues.ts | 4 + services/libs/types/src/enums/index.ts | 1 + services/libs/types/src/index.ts | 2 + 18 files changed, 369 insertions(+) create mode 100644 backend/src/api/member/memberDataIssueCreate.ts create mode 100644 backend/src/api/organization/organizationDataIssueCreate.ts create mode 100644 backend/src/database/migrations/U1727097932__createDataIssuesTable.sql create mode 100644 backend/src/database/migrations/V1727097932__createDataIssuesTable.sql create mode 100644 backend/src/services/dataIssueService.ts create mode 100644 services/libs/data-access-layer/src/data_issues/index.ts create mode 100644 services/libs/types/src/dataIssues.ts create mode 100644 services/libs/types/src/enums/dataIssues.ts diff --git a/backend/config/custom-environment-variables.json b/backend/config/custom-environment-variables.json index 1958592872..14f884d74d 100644 --- a/backend/config/custom-environment-variables.json +++ b/backend/config/custom-environment-variables.json @@ -130,6 +130,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" }, diff --git a/backend/package.json b/backend/package.json index e7702a36b8..62c32f5c98 100644 --- a/backend/package.json +++ b/backend/package.json @@ -69,6 +69,7 @@ "@cubejs-client/core": "^0.30.4", "@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", diff --git a/backend/src/api/member/index.ts b/backend/src/api/member/index.ts index f4fe8f9930..9425cc2c34 100644 --- a/backend/src/api/member/index.ts +++ b/backend/src/api/member/index.ts @@ -49,4 +49,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), + ) } diff --git a/backend/src/api/member/memberDataIssueCreate.ts b/backend/src/api/member/memberDataIssueCreate.ts new file mode 100644 index 0000000000..70e5872c90 --- /dev/null +++ b/backend/src/api/member/memberDataIssueCreate.ts @@ -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) +} diff --git a/backend/src/api/organization/index.ts b/backend/src/api/organization/index.ts index 031d96bc30..96c5ba9b00 100644 --- a/backend/src/api/organization/index.ts +++ b/backend/src/api/organization/index.ts @@ -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), + ) } diff --git a/backend/src/api/organization/organizationDataIssueCreate.ts b/backend/src/api/organization/organizationDataIssueCreate.ts new file mode 100644 index 0000000000..695d642e4f --- /dev/null +++ b/backend/src/api/organization/organizationDataIssueCreate.ts @@ -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) +} diff --git a/backend/src/conf/configTypes.ts b/backend/src/conf/configTypes.ts index 7f4baf6f3e..a6ef050da5 100644 --- a/backend/src/conf/configTypes.ts +++ b/backend/src/conf/configTypes.ts @@ -141,6 +141,11 @@ export interface GithubConfiguration { callbackUrl: string } +export interface GithubIssueReporterConfiguration { + appId: string + privateKey: string + installationId: string +} export interface SendgridConfiguration { key: string webhookSigningSecret: string diff --git a/backend/src/conf/index.ts b/backend/src/conf/index.ts index ba1d4f1952..45834a175a 100644 --- a/backend/src/conf/index.ts +++ b/backend/src/conf/index.ts @@ -42,6 +42,7 @@ import { GithubTokenConfiguration, GitlabConfiguration, IRedditConfig, + GithubIssueReporterConfiguration, } from './configTypes' // TODO-kube @@ -114,6 +115,9 @@ export const DISCORD_CONFIG: DiscordConfiguration = config.get('github') +export const GITHUB_ISSUE_REPORTER_CONFIG: GithubIssueReporterConfiguration = + config.get('githubIssueReporter') + export const SENDGRID_CONFIG: SendgridConfiguration = config.get('sendgrid') export const NETLIFY_CONFIG: NetlifyConfiguration = config.get('netlify') diff --git a/backend/src/database/migrations/U1727097932__createDataIssuesTable.sql b/backend/src/database/migrations/U1727097932__createDataIssuesTable.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql new file mode 100644 index 0000000000..2e77cc47b1 --- /dev/null +++ b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql @@ -0,0 +1,14 @@ +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, + primary key ("id"), + foreign key ("createdById") references users (id) on delete cascade +); \ No newline at end of file diff --git a/backend/src/security/permissions.ts b/backend/src/security/permissions.ts index ef216b2233..53d33b2e8d 100644 --- a/backend/src/security/permissions.ts +++ b/backend/src/security/permissions.ts @@ -1206,6 +1206,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, + ], + }, } } diff --git a/backend/src/services/dataIssueService.ts b/backend/src/services/dataIssueService.ts new file mode 100644 index 0000000000..94f490c482 --- /dev/null +++ b/backend/src/services/dataIssueService.ts @@ -0,0 +1,123 @@ +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 { API_CONFIG, 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 entityUrl: string + let reportedBy: string + + if (data.entity === DataIssueEntity.ORGANIZATION) { + const organization = await findOrgById(qx, entityId, [ + OrganizationField.ID, + OrganizationField.DISPLAY_NAME, + ]) + entityName = organization.displayName + entityUrl = `${API_CONFIG.url}/organizations/${organization.id}` + } else if (data.entity === DataIssueEntity.PERSON) { + const member = await findMemberById(qx, entityId, [MemberField.ID, MemberField.DISPLAY_NAME]) + entityName = member.displayName + entityUrl = `${API_CONFIG.url}/members/${member.id}` + } 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[${entityUrl}](${entityUrl})\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, + profileUrl: entityUrl, + }) + + 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 { + const octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId, + privateKey, + installationId, + }, + }) + + const authResponse = await octokit.auth({ + type: 'installation', + installationId, + }) + + return (authResponse as InstallationAccessTokenData).token + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40982f3f63..6087352907 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: '@octokit/auth-app': specifier: ^3.6.1 version: 3.6.1(encoding@0.1.13) + '@octokit/core': + specifier: ^6.1.2 + version: 6.1.2 '@octokit/graphql': specifier: ^4.8.0 version: 4.8.0(encoding@0.1.13) @@ -3471,6 +3474,18 @@ packages: resolution: {integrity: sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg==} engines: {node: '>= 14'} + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + '@octokit/endpoint@6.0.12': resolution: {integrity: sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==} @@ -3485,6 +3500,10 @@ packages: resolution: {integrity: sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==} engines: {node: '>= 14'} + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + '@octokit/oauth-authorization-url@4.3.3': resolution: {integrity: sha512-lhP/t0i8EwTmayHG4dqLXgU+uPVys4WD/qUNvC+HfB1S1dyqULm5Yx9uKc1x79aP66U1Cb4OZeW8QU/RA9A4XA==} @@ -3505,6 +3524,9 @@ packages: '@octokit/openapi-types@18.1.1': resolution: {integrity: sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==} + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + '@octokit/request-error@2.1.0': resolution: {integrity: sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==} @@ -3512,6 +3534,10 @@ packages: resolution: {integrity: sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==} engines: {node: '>= 14'} + '@octokit/request-error@6.1.4': + resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==} + engines: {node: '>= 18'} + '@octokit/request@5.6.3': resolution: {integrity: sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==} @@ -3519,6 +3545,13 @@ packages: resolution: {integrity: sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==} engines: {node: '>= 14'} + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/types@13.5.0': + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} @@ -5007,6 +5040,9 @@ packages: resolution: {integrity: sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==} engines: {node: '>= 10.0.0'} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -9375,6 +9411,9 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -11966,6 +12005,23 @@ snapshots: transitivePeerDependencies: - encoding + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + '@octokit/endpoint@6.0.12': dependencies: '@octokit/types': 6.41.0 @@ -11994,6 +12050,12 @@ snapshots: transitivePeerDependencies: - encoding + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + '@octokit/oauth-authorization-url@4.3.3': {} '@octokit/oauth-authorization-url@5.0.0': {} @@ -12022,6 +12084,8 @@ snapshots: '@octokit/openapi-types@18.1.1': {} + '@octokit/openapi-types@22.2.0': {} + '@octokit/request-error@2.1.0': dependencies: '@octokit/types': 6.41.0 @@ -12034,6 +12098,10 @@ snapshots: deprecation: 2.3.1 once: 1.4.0 + '@octokit/request-error@6.1.4': + dependencies: + '@octokit/types': 13.5.0 + '@octokit/request@5.6.3(encoding@0.1.13)': dependencies: '@octokit/endpoint': 6.0.12 @@ -12056,6 +12124,17 @@ snapshots: transitivePeerDependencies: - encoding + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/types@13.5.0': + dependencies: + '@octokit/openapi-types': 22.2.0 + '@octokit/types@6.41.0': dependencies: '@octokit/openapi-types': 12.11.0 @@ -14037,6 +14116,8 @@ snapshots: transitivePeerDependencies: - supports-color + before-after-hook@3.0.2: {} + big-integer@1.6.52: {} bignumber.js@9.1.2: {} @@ -19246,6 +19327,8 @@ snapshots: universal-user-agent@6.0.1: {} + universal-user-agent@7.0.2: {} + universalify@0.1.2: {} universalify@2.0.1: {} diff --git a/services/libs/data-access-layer/src/data_issues/index.ts b/services/libs/data-access-layer/src/data_issues/index.ts new file mode 100644 index 0000000000..83b2ec75db --- /dev/null +++ b/services/libs/data-access-layer/src/data_issues/index.ts @@ -0,0 +1,60 @@ +import { generateUUIDv4 } from '@crowd/common' +import { QueryExecutor } from '../queryExecutor' +import { IDataIssue } from '@crowd/types' +import { QueryTypes } from 'sequelize' +import { QueryResult, queryTableById } from '../utils' + +export interface IDbInsertDataIssuePayload { + entity: string + profileUrl: string + dataIssue: string + dataType: string + description: string + githubIssueUrl: string + createdById: string +} + +export enum DataIssueField { + // meta + ID = 'id', + ENTITY = 'entity', + PROFILE_URL = 'profileUrl', + DATA_ISSUE = 'dataIssue', + DATA_TYPE = 'dataType', + DESCRIPTION = 'description', + GITHUB_ISSUE_URL = 'githubIssueUrl', + CREATED_BY_ID = 'createdById', + CREATED_AT = 'createdAt', + UPDATED_AT = 'updatedAt', +} + +export async function createDataIssue( + qx: QueryExecutor, + data: IDbInsertDataIssuePayload, +): Promise { + const id = generateUUIDv4() + await qx.result( + `insert into "dataIssues" ("id", "entity", "profileUrl", "dataIssue", "dataType", "description", "githubIssueUrl", "createdById") + values ($(id), $(entity), $(profileUrl), $(dataIssue), $(dataType), $(description), $(githubIssueUrl), $(createdById))`, + { + id, + entity: data.entity, + profileUrl: data.profileUrl, + dataIssue: data.dataIssue, + dataType: data.dataType, + description: data.description, + githubIssueUrl: data.githubIssueUrl, + createdById: data.createdById, + }, + ) + + return findDataIssueById(qx, id, Object.values(DataIssueField)) +} + +export async function findDataIssueById( + qx: QueryExecutor, + dataIssueId: string, + fields: T[], +): Promise> { + return queryTableById(qx, 'dataIssues', Object.values(DataIssueField), dataIssueId, fields) +} diff --git a/services/libs/types/src/dataIssues.ts b/services/libs/types/src/dataIssues.ts new file mode 100644 index 0000000000..e7896b6c5d --- /dev/null +++ b/services/libs/types/src/dataIssues.ts @@ -0,0 +1,14 @@ +import { DataIssueEntity } from './enums' + +export interface IDataIssue { + id: string + entity: DataIssueEntity + profileUrl: string + dataIssue: string + dataType: string + description: string + githubIssueUrl: string + createdById: string + createdAt: Date + updatedAt: Date +} diff --git a/services/libs/types/src/enums/dataIssues.ts b/services/libs/types/src/enums/dataIssues.ts new file mode 100644 index 0000000000..2e45990d6e --- /dev/null +++ b/services/libs/types/src/enums/dataIssues.ts @@ -0,0 +1,4 @@ +export enum DataIssueEntity { + ORGANIZATION = 'organization', + PERSON = 'person', +} diff --git a/services/libs/types/src/enums/index.ts b/services/libs/types/src/enums/index.ts index 0e657b9771..bfca376b36 100644 --- a/services/libs/types/src/enums/index.ts +++ b/services/libs/types/src/enums/index.ts @@ -16,3 +16,4 @@ export * from './dashboard' export * from './merging' export * from './suggestions' export * from './exports' +export * from './dataIssues' diff --git a/services/libs/types/src/index.ts b/services/libs/types/src/index.ts index 083503367e..cbc4e1f455 100644 --- a/services/libs/types/src/index.ts +++ b/services/libs/types/src/index.ts @@ -54,3 +54,5 @@ export * from './enums' export * from './service' export * from './productAnalytics' + +export * from './dataIssues' From 02221e15de7c8bedc6b24f46696a0f647fb98da5 Mon Sep 17 00:00:00 2001 From: anilb Date: Tue, 24 Sep 2024 16:09:33 +0200 Subject: [PATCH 2/9] small fixes, started working on issue webhooks for data issue resolutions --- .../V1727097932__createDataIssuesTable.sql | 2 ++ backend/src/services/dataIssueService.ts | 4 +-- .../apps/webhook_api/src/routes/dataIssue.ts | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 services/apps/webhook_api/src/routes/dataIssue.ts diff --git a/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql index 2e77cc47b1..158057ff98 100644 --- a/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql +++ b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql @@ -9,6 +9,8 @@ create table public."dataIssues" ( "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" timestamp with time zone default null, primary key ("id"), foreign key ("createdById") references users (id) on delete cascade ); \ No newline at end of file diff --git a/backend/src/services/dataIssueService.ts b/backend/src/services/dataIssueService.ts index 94f490c482..6cf624b3fc 100644 --- a/backend/src/services/dataIssueService.ts +++ b/backend/src/services/dataIssueService.ts @@ -50,11 +50,11 @@ export default class DataIssueService extends LoggerBase { OrganizationField.DISPLAY_NAME, ]) entityName = organization.displayName - entityUrl = `${API_CONFIG.url}/organizations/${organization.id}` + entityUrl = `${API_CONFIG.frontendUrl}/organizations/${organization.id}` } else if (data.entity === DataIssueEntity.PERSON) { const member = await findMemberById(qx, entityId, [MemberField.ID, MemberField.DISPLAY_NAME]) entityName = member.displayName - entityUrl = `${API_CONFIG.url}/members/${member.id}` + entityUrl = `${API_CONFIG.frontendUrl}/members/${member.id}` } else { throw new Error(`Unsupported data issue entity ${data.entity}!1`) } diff --git a/services/apps/webhook_api/src/routes/dataIssue.ts b/services/apps/webhook_api/src/routes/dataIssue.ts new file mode 100644 index 0000000000..371c26c1b9 --- /dev/null +++ b/services/apps/webhook_api/src/routes/dataIssue.ts @@ -0,0 +1,34 @@ +import { asyncWrap } from '../middleware/error' +import { Error400BadRequest } from '@crowd/common' +import express from 'express' + +const SIGNATURE_HEADER = 'x-hub-signature' +const EVENT_HEADER = 'x-github-event' + +export const installGithubRoutes = async (app: express.Express) => { + app.post( + '/github', + asyncWrap(async (req, res) => { + if (!req.headers[SIGNATURE_HEADER]) { + throw new Error400BadRequest('Missing signature header!') + } + const signature = req.headers['x-hub-signature'] + + if (!req.headers[EVENT_HEADER]) { + throw new Error400BadRequest('Missing event header!') + } + const event = req.headers['x-github-event'] + + const data = req.body + if (!data.installation?.id) { + throw new Error400BadRequest('Missing installation id!') + } + const identifier = data.installation.id.toString() + + // check event - is it issue close? + // if issue close, find data issue by issue url in (dataIssues) + // if found send resolution email to dataIssue.createdById.email + // afterwards set dataIssue.resolutionEmailSentAt dataIssuel.resolutionEmailSentTo + }), + ) +} From b005851f4c19877e366cc2928019e8727e66b6a5 Mon Sep 17 00:00:00 2001 From: anilb Date: Thu, 26 Sep 2024 11:19:44 +0200 Subject: [PATCH 3/9] get profileUrl from request body --- backend/src/services/dataIssueService.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/src/services/dataIssueService.ts b/backend/src/services/dataIssueService.ts index 6cf624b3fc..8b6d4ccc97 100644 --- a/backend/src/services/dataIssueService.ts +++ b/backend/src/services/dataIssueService.ts @@ -10,7 +10,7 @@ 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 { API_CONFIG, GITHUB_ISSUE_REPORTER_CONFIG } from '@/conf' +import { GITHUB_ISSUE_REPORTER_CONFIG } from '@/conf' export interface IDataIssueCreatePayload { entity: DataIssueEntity @@ -41,7 +41,6 @@ export default class DataIssueService extends LoggerBase { const user = SequelizeRepository.getCurrentUser(this.options) let entityName: string - let entityUrl: string let reportedBy: string if (data.entity === DataIssueEntity.ORGANIZATION) { @@ -50,11 +49,9 @@ export default class DataIssueService extends LoggerBase { OrganizationField.DISPLAY_NAME, ]) entityName = organization.displayName - entityUrl = `${API_CONFIG.frontendUrl}/organizations/${organization.id}` } else if (data.entity === DataIssueEntity.PERSON) { const member = await findMemberById(qx, entityId, [MemberField.ID, MemberField.DISPLAY_NAME]) entityName = member.displayName - entityUrl = `${API_CONFIG.frontendUrl}/members/${member.id}` } else { throw new Error(`Unsupported data issue entity ${data.entity}!1`) } @@ -81,7 +78,7 @@ export default class DataIssueService extends LoggerBase { title: `[Data Issue] ${entityName} (${data.entity[0].toUpperCase()}${data.entity .slice(1) .toLowerCase()})`, - body: `**Entity**\n${entityName}\n\n**Profile**\n[${entityUrl}](${entityUrl})\n\n**Data Issue**\n${data.dataIssue}\n\n**Description**\n${data.description}\n\n**Reported by**\n${reportedBy}`, + 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'], }, ) @@ -89,7 +86,6 @@ export default class DataIssueService extends LoggerBase { ...data, githubIssueUrl: result.data.html_url, createdById: user.id, - profileUrl: entityUrl, }) return res From 82171de558874b0ec62db24ce746b71feb1c077f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Thu, 26 Sep 2024 12:46:52 +0200 Subject: [PATCH 4/9] Report modal display --- frontend/config/styles/components/radio.scss | 2 +- .../details/contributor-details-actions.vue | 16 +- .../modules/layout/components/menu/menu.vue | 21 +- .../permissions/helpers/usePermissions.ts | 7 +- .../component/report-data-issue-modal.vue | 201 ++++++++++++++++++ .../config/entity/organization.ts | 14 ++ .../report-issue/config/entity/person.ts | 16 ++ .../modules/report-issue/config/index.ts | 32 +++ .../report-issue/config/type/type-domain.vue | 14 ++ .../config/type/type-identity.vue | 40 ++++ .../config/type/type-organization-details.vue | 34 +++ .../config/type/type-profile-details.vue | 34 +++ .../config/type/type-project-affiliation.vue | 11 + .../config/type/type-work-experience.vue | 32 +++ .../constants/report-data-entity.enum.ts | 4 + .../constants/report-data-type.enum.ts | 10 + frontend/src/ui-kit/input/input.scss | 4 +- frontend/src/ui-kit/radio/Radio.vue | 4 +- frontend/src/ui-kit/textarea/Textarea.vue | 37 ++++ 19 files changed, 518 insertions(+), 15 deletions(-) create mode 100644 frontend/src/shared/modules/report-issue/component/report-data-issue-modal.vue create mode 100644 frontend/src/shared/modules/report-issue/config/entity/organization.ts create mode 100644 frontend/src/shared/modules/report-issue/config/entity/person.ts create mode 100644 frontend/src/shared/modules/report-issue/config/index.ts create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-domain.vue create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-identity.vue create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-organization-details.vue create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-profile-details.vue create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-project-affiliation.vue create mode 100644 frontend/src/shared/modules/report-issue/config/type/type-work-experience.vue create mode 100644 frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts create mode 100644 frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts create mode 100644 frontend/src/ui-kit/textarea/Textarea.vue diff --git a/frontend/config/styles/components/radio.scss b/frontend/config/styles/components/radio.scss index ee80dd3b9f..a22ebce916 100644 --- a/frontend/config/styles/components/radio.scss +++ b/frontend/config/styles/components/radio.scss @@ -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 diff --git a/frontend/src/modules/contributor/components/details/contributor-details-actions.vue b/frontend/src/modules/contributor/components/details/contributor-details-actions.vue index d8178887a9..81d6ca4968 100644 --- a/frontend/src/modules/contributor/components/details/contributor-details-actions.vue +++ b/frontend/src/modules/contributor/components/details/contributor-details-actions.vue @@ -2,6 +2,13 @@
+ + Report data issue + + + + diff --git a/frontend/src/shared/modules/report-issue/config/entity/organization.ts b/frontend/src/shared/modules/report-issue/config/entity/organization.ts new file mode 100644 index 0000000000..3cf8513a0c --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/entity/organization.ts @@ -0,0 +1,14 @@ +import { ReportDataConfig } from '@/shared/modules/report-issue/config/index'; +import { ReportDataType } from '@/shared/modules/report-issue/constants/report-data-type.enum'; + +const person: ReportDataConfig = { + url: (id: string) => `/organization/${id}/data-issue`, + types: [ + ReportDataType.ORGANIZATION_DETAILS, + ReportDataType.IDENTITY, + ReportDataType.DOMAIN, + ReportDataType.OTHER, + ], +}; + +export default person; diff --git a/frontend/src/shared/modules/report-issue/config/entity/person.ts b/frontend/src/shared/modules/report-issue/config/entity/person.ts new file mode 100644 index 0000000000..7b70f3db83 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/entity/person.ts @@ -0,0 +1,16 @@ +import { ReportDataConfig } from '@/shared/modules/report-issue/config/index'; +import { ReportDataType } from '@/shared/modules/report-issue/constants/report-data-type.enum'; + +const person: ReportDataConfig = { + url: (id: string) => `/member/${id}/data-issue`, + types: [ + ReportDataType.PROFILE_DETAILS, + ReportDataType.PROJECT, + ReportDataType.PROJECT_AFFILIATION, + ReportDataType.WORK_EXPERIENCE, + ReportDataType.IDENTITY, + ReportDataType.OTHER, + ], +}; + +export default person; diff --git a/frontend/src/shared/modules/report-issue/config/index.ts b/frontend/src/shared/modules/report-issue/config/index.ts new file mode 100644 index 0000000000..ddcd6c2806 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/index.ts @@ -0,0 +1,32 @@ +import { ReportDataEntity } from '@/shared/modules/report-issue/constants/report-data-entity.enum'; + +import person from './entity/person'; +import organization from './entity/organization'; + +import ProfileDetails from './type/type-profile-details.vue'; +import ProjectAffiliation from './type/type-project-affiliation.vue'; +import WorkExperience from './type/type-work-experience.vue'; +import Identity from './type/type-identity.vue'; +import OrganizationDetails from './type/type-organization-details.vue'; +import Domain from './type/type-domain.vue'; + +export interface ReportDataConfig { + url: (id: string) => string; + types: DataReportTypes[]; +} + +export const reportDataConfig: Record = { + person, + organization, +}; + +export const reportDataTypeDisplay: Record = { + [DataReportTypes.PROFILE_DETAILS]: ProfileDetails, + [DataReportTypes.PROJECT]: null, + [DataReportTypes.PROJECT_AFFILIATION]: ProjectAffiliation, + [DataReportTypes.WORK_EXPERIENCE]: WorkExperience, + [DataReportTypes.IDENTITY]: Identity, + [DataReportTypes.ORGANIZATION_DETAILS]: OrganizationDetails, + [DataReportTypes.DOMAIN]: Domain, + [DataReportTypes.OTHER]: null, +}; diff --git a/frontend/src/shared/modules/report-issue/config/type/type-domain.vue b/frontend/src/shared/modules/report-issue/config/type/type-domain.vue new file mode 100644 index 0000000000..139fc8bbb7 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-domain.vue @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/type-identity.vue b/frontend/src/shared/modules/report-issue/config/type/type-identity.vue new file mode 100644 index 0000000000..ebb86e8e1a --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-identity.vue @@ -0,0 +1,40 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/type-organization-details.vue b/frontend/src/shared/modules/report-issue/config/type/type-organization-details.vue new file mode 100644 index 0000000000..6395f6dda4 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-organization-details.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/type-profile-details.vue b/frontend/src/shared/modules/report-issue/config/type/type-profile-details.vue new file mode 100644 index 0000000000..6767c696e5 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-profile-details.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/type-project-affiliation.vue b/frontend/src/shared/modules/report-issue/config/type/type-project-affiliation.vue new file mode 100644 index 0000000000..4314394301 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-project-affiliation.vue @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/type-work-experience.vue b/frontend/src/shared/modules/report-issue/config/type/type-work-experience.vue new file mode 100644 index 0000000000..274623a210 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/type-work-experience.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts b/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts new file mode 100644 index 0000000000..d2785f1328 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts @@ -0,0 +1,4 @@ +export enum ReportDataEntity { + PERSON = 'person', + ORGANIZATION = 'organization', +} diff --git a/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts b/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts new file mode 100644 index 0000000000..6bd7c06f27 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts @@ -0,0 +1,10 @@ +export enum ReportDataType { + PROFILE_DETAILS = 'Profile details', + PROJECT = 'Project', + PROJECT_AFFILIATION = 'Project affiliation', + WORK_EXPERIENCE = 'Work experience', + IDENTITY = 'Identity', + ORGANIZATION_DETAILS = 'Organization details', + DOMAIN = 'Domain', + OTHER = 'Other', +} diff --git a/frontend/src/ui-kit/input/input.scss b/frontend/src/ui-kit/input/input.scss index a2ce1e6e05..6cbde98338 100644 --- a/frontend/src/ui-kit/input/input.scss +++ b/frontend/src/ui-kit/input/input.scss @@ -19,9 +19,9 @@ --lf-input-border: var(--lf-input-invalid-border) } - input{ + input, textarea{ color: var(--lf-input-color); - @apply text-sm leading-6 font-normal p-2 outline-none appearance-none flex-grow block w-full transition-all bg-transparent; + @apply text-sm leading-6 font-normal p-2 outline-none appearance-none flex-grow block w-full transition-all bg-transparent resize-none; &::placeholder, &:-ms-input-placeholder, &::-ms-input-placeholder { color: var(--lf-input-placeholder-color); diff --git a/frontend/src/ui-kit/radio/Radio.vue b/frontend/src/ui-kit/radio/Radio.vue index 967749af43..70d2a764fa 100644 --- a/frontend/src/ui-kit/radio/Radio.vue +++ b/frontend/src/ui-kit/radio/Radio.vue @@ -5,7 +5,7 @@ `c-radio--${props.size}`, ]" > - + @@ -20,10 +20,12 @@ const props = withDefaults(defineProps<{ size?: RadioSize, modelValue: string, value?: string, + name?: string, disabled?: boolean, }>(), { size: 'medium', value: '', + name: '', disabled: false, }); diff --git a/frontend/src/ui-kit/textarea/Textarea.vue b/frontend/src/ui-kit/textarea/Textarea.vue new file mode 100644 index 0000000000..fc09c4aad7 --- /dev/null +++ b/frontend/src/ui-kit/textarea/Textarea.vue @@ -0,0 +1,37 @@ +