-
Notifications
You must be signed in to change notification settings - Fork 4
404 add reputation score api endpoint #405
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||
| import { Request, Response } from 'express'; | ||||||||||||||
| import { catchAsync } from '../utils'; | ||||||||||||||
| import parentLogger from '../config/logger'; | ||||||||||||||
| import { moduleService, platformService, ociService } from '../services'; | ||||||||||||||
| import { ApiError } from '../utils'; | ||||||||||||||
| import { IModule, IPlatform } from '@togethercrew.dev/db'; | ||||||||||||||
| import { HydratedDocument } from 'mongoose'; | ||||||||||||||
| import * as Neo4j from '../neo4j'; | ||||||||||||||
| import { NEO4J_PLATFORM_INFO } from '../constants/neo4j.constant'; | ||||||||||||||
| import { SupportedNeo4jPlatforms } from '../types/neo4j.type'; | ||||||||||||||
|
|
||||||||||||||
| const logger = parentLogger.child({ module: 'NftController' }); | ||||||||||||||
|
|
||||||||||||||
| const getReputationScore = catchAsync(async function (req: Request, res: Response) { | ||||||||||||||
| const { tokenId, address } = req.params; | ||||||||||||||
| const supportedPlatforms = ['discord', 'discourse']; | ||||||||||||||
|
|
||||||||||||||
| let repuationScore; | ||||||||||||||
| logger.debug(tokenId, address); | ||||||||||||||
| const profiles: Array<any> = await getProfilesOnAllSupportedChains(address); | ||||||||||||||
| logger.debug(profiles); | ||||||||||||||
| const dynamicNftModule = await moduleService.getModuleByFilter({ 'options.platforms.0.metadata.tokenId': tokenId }); | ||||||||||||||
| logger.debug(dynamicNftModule); | ||||||||||||||
|
|
||||||||||||||
| for (let i = 0; i < supportedPlatforms.length; i++) { | ||||||||||||||
| const platform = await platformService.getPlatformByFilter({ | ||||||||||||||
| name: supportedPlatforms[i], | ||||||||||||||
| community: dynamicNftModule?.community, | ||||||||||||||
| }); | ||||||||||||||
| logger.debug({ i, platform, supportedPlatforms: supportedPlatforms[i] }); | ||||||||||||||
| for (let j = 0; j < profiles.length; j++) { | ||||||||||||||
| const profile = profiles[j]; | ||||||||||||||
| logger.debug({ i, j, profile, supportedPlatforms: supportedPlatforms[i] }); | ||||||||||||||
| const temp = platform?.name as SupportedNeo4jPlatforms; | ||||||||||||||
| if (profile.profile.provider === supportedPlatforms[i]) { | ||||||||||||||
| const reputationScoreQuery = ` | ||||||||||||||
| MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: "${profile.profile.id}"})-[r:HAVE_METRICS {platformId: "${platform?.id}"}]->(a) | ||||||||||||||
| WITH r.date as metrics_date, r.closenessCentrality as memberScore | ||||||||||||||
| ORDER BY metrics_date DESC | ||||||||||||||
| LIMIT 1 | ||||||||||||||
| MATCH (user:${NEO4J_PLATFORM_INFO[temp].member})-[user_r:HAVE_METRICS {platformId: "${platform?.id}", date: metrics_date}]->(user) | ||||||||||||||
| WITH memberScore, MAX(user_r.closenessCentrality) as maxScore | ||||||||||||||
| RETURN memberScore / maxScore AS reputation_score | ||||||||||||||
| `; | ||||||||||||||
|
Comment on lines
+36
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent injection attacks by parameterizing Neo4j queries Directly interpolating variables into Neo4j query strings can lead to injection vulnerabilities if any variables contain malicious input. Use parameterized queries to securely pass variables to the query. Refactor the code to use parameterized queries: - const reputationScoreQuery = `
- MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: "${profile.profile.id}"})-[r:HAVE_METRICS {platformId: "${platform?.id}"}]->(a)
- WITH r.date as metrics_date, r.closenessCentrality as memberScore
- ORDER BY metrics_date DESC
- LIMIT 1
- MATCH (user:${NEO4J_PLATFORM_INFO[temp].member})-[user_r:HAVE_METRICS {platformId: "${platform?.id}", date: metrics_date}]->(user)
- WITH memberScore, MAX(user_r.closenessCentrality) as maxScore
- RETURN memberScore / maxScore AS reputation_score
- `;
+ const reputationScoreQuery = `
+ MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: $profileId})-[r:HAVE_METRICS {platformId: $platformId}]->(a)
+ WITH r.date as metrics_date, r.closenessCentrality as memberScore
+ ORDER BY metrics_date DESC
+ LIMIT 1
+ MATCH (user:${NEO4J_PLATFORM_INFO[temp].member})-[user_r:HAVE_METRICS {platformId: $platformId, date: metrics_date}]->(user)
+ WITH memberScore, MAX(user_r.closenessCentrality) as maxScore
+ RETURN memberScore / maxScore AS reputation_score
+ `;
...
- const neo4jData = await Neo4j.read(reputationScoreQuery);
+ const neo4jData = await Neo4j.read(reputationScoreQuery, {
+ profileId: profile.profile.id,
+ platformId: platform?.id,
+ });This refactoring ensures that variables are passed safely to the query, mitigating the risk of injection attacks.
|
||||||||||||||
|
|
||||||||||||||
| const neo4jData = await Neo4j.read(reputationScoreQuery); | ||||||||||||||
| const { records } = neo4jData; | ||||||||||||||
| logger.debug(records); | ||||||||||||||
|
|
||||||||||||||
| const reputationScoreResponse = records[0]; | ||||||||||||||
|
|
||||||||||||||
| logger.debug(reputationScoreResponse); | ||||||||||||||
|
|
||||||||||||||
| const { _fieldLookup, _fields } = reputationScoreResponse as unknown as { | ||||||||||||||
| _fieldLookup: Record<string, number>; | ||||||||||||||
| _fields: number[]; | ||||||||||||||
| }; | ||||||||||||||
|
Comment on lines
+54
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using internal properties of Neo4j response objects Accessing internal properties like Refactor the code to use public methods: - const { _fieldLookup, _fields } = reputationScoreResponse as unknown as {
- _fieldLookup: Record<string, number>;
- _fields: number[];
- };
- repuationScore = _fields[_fieldLookup['reputation_score']];
+ const reputationScoreRecord = reputationScoreResponse.get('reputation_score');
+ repuationScore = reputationScoreRecord;This approach uses the 📝 Committable suggestion
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| repuationScore = _fields[_fieldLookup['reputation_score']]; | ||||||||||||||
| logger.debug(repuationScore); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| return repuationScore; | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| async function getProfilesOnAllSupportedChains(address: string) { | ||||||||||||||
| let profiles: Array<any> = []; | ||||||||||||||
| const supportedChainIds = [11155111]; | ||||||||||||||
| for (let i = 0; i < supportedChainIds.length; i++) { | ||||||||||||||
| const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]); | ||||||||||||||
| profiles = profiles.concat(chainProfiles); | ||||||||||||||
|
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle potential errors from OCI service calls The call to Add error handling for the OCI service call: for (let i = 0; i < supportedChainIds.length; i++) {
- const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
+ let chainProfiles;
+ try {
+ chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
+ } catch (error) {
+ logger.error(`Failed to get profiles for chain ID ${supportedChainIds[i]}:`, error);
+ continue;
+ }
profiles = profiles.concat(chainProfiles);
}This ensures that a failure in one chain does not halt the entire process.
|
||||||||||||||
| } | ||||||||||||||
| return profiles; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function shouldProfilesExist(profiles: Array<any>) { | ||||||||||||||
| if (profiles.length < 0) { | ||||||||||||||
| throw new ApiError(400, 'User has no any onchain profiles'); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function shouldDynamicNftModuleExist(dynamicNftModule: HydratedDocument<IModule> | null) { | ||||||||||||||
| if (!dynamicNftModule) { | ||||||||||||||
| throw new ApiError(400, "There's not any assoicated dynamic nft module to the token Id"); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function shouldPlatformExist(platform: HydratedDocument<IPlatform> | null) { | ||||||||||||||
| if (!platform) { | ||||||||||||||
| throw new ApiError(400, "There's not any platform connected for requested platform"); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function shouldProfileExist(profile: any) { | ||||||||||||||
| if (!profile) { | ||||||||||||||
| throw new ApiError(400, "There's not any user oncahin profile for requested platform"); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| export default { | ||||||||||||||
| getReputationScore, | ||||||||||||||
| }; | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import express from 'express'; | ||
| import { nftController } from '../../controllers'; | ||
| import { nftValidation } from '../../validations'; | ||
| import { validate } from '../../middlewares'; | ||
| const router = express.Router(); | ||
|
|
||
| // Routes | ||
| router.post( | ||
| '/:tokenId/:address/reputation-score', | ||
| validate(nftValidation.getReputationScore), | ||
| nftController.getReputationScore, | ||
| ); | ||
|
|
||
| export default router; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { HydratedDocument, Types } from 'mongoose'; | ||
| import httpStatus from 'http-status'; | ||
| import { Platform, IPlatform } from '@togethercrew.dev/db'; | ||
| import ApiError from '../utils/ApiError'; | ||
| import sagaService from './saga.service'; | ||
| import discourseService from './discourse'; | ||
| import { Snowflake } from 'discord.js'; | ||
| import { analyzerAction, analyzerWindow } from '../config/analyzer.statics'; | ||
| import { PlatformNames } from '@togethercrew.dev/db'; | ||
|
|
||
| /** | ||
| * get reputation score | ||
| * @param {IPlatform} PlatformBody | ||
| * @returns {Promise<HydratedDocument<IPlatform>>} | ||
| */ | ||
| const getReputationScore = async (PlatformBody: IPlatform): Promise<HydratedDocument<IPlatform>> => { | ||
| if (PlatformBody.name === PlatformNames.Discord || PlatformBody.name === PlatformNames.Discourse) { | ||
| if (PlatformBody.metadata) { | ||
| PlatformBody.metadata = { | ||
| action: analyzerAction, | ||
| window: analyzerWindow, | ||
| ...PlatformBody.metadata, | ||
| }; | ||
| } | ||
| } | ||
| const platform = await Platform.create(PlatformBody); | ||
| if (PlatformBody.name === PlatformNames.Discord) { | ||
| await sagaService.createAndStartFetchMemberSaga(platform._id); | ||
| } | ||
| return platform; | ||
| }; | ||
|
|
||
| export default { | ||
| getReputationScore, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import fetch from 'node-fetch'; | ||
| import config from '../config'; | ||
| import { ApiError } from '../utils'; | ||
| import parentLogger from '../config/logger'; | ||
|
|
||
| const logger = parentLogger.child({ module: 'OciService' }); | ||
|
|
||
| async function getProfiles(address: string, chainId: number) { | ||
| try { | ||
| logger.debug(`${config.ociBackendURL}/oci/profiles/${chainId}/${address}`); | ||
| const response = await fetch(`${config.ociBackendURL}/api/v1/oci/profiles/${chainId}/${address}`, { | ||
| method: 'GET', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| if (response.ok) { | ||
| return await response.json(); | ||
| } else { | ||
| const errorResponse = await response.text(); | ||
| throw new Error(errorResponse); | ||
| } | ||
| } catch (error: any) { | ||
| logger.error(error, 'Failed to get profiles from oci backend'); | ||
| throw new ApiError(590, 'Failed to get profiles from oci backend '); | ||
| } | ||
| } | ||
|
|
||
| export default { | ||
| getProfiles, | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,15 @@ | ||||||||
| import Joi from 'joi'; | ||||||||
| import { PlatformNames } from '@togethercrew.dev/db'; | ||||||||
|
|
||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove unused import The import Joi from 'joi';
-import { PlatformNames } from '@togethercrew.dev/db';📝 Committable suggestion
Suggested change
|
||||||||
| const getReputationScore = { | ||||||||
| params: Joi.object().keys({ | ||||||||
| tokenId: Joi.string().required(), | ||||||||
| address: Joi.string() | ||||||||
| .regex(/^0x[a-fA-F0-9]{40}$/) | ||||||||
| .required(), | ||||||||
| }), | ||||||||
| }; | ||||||||
|
|
||||||||
| export default { | ||||||||
| getReputationScore, | ||||||||
| }; | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle potential undefined 'repuationScore' before returning
If none of the profiles match the supported platforms,
repuationScoremay remainundefined, leading to unexpected behavior when returned. InitializerepuationScoreto a default value and ensure it is always defined before returning.Apply the following diff to initialize
repuationScoreand handle the response correctly:Also applies to: 64-64