diff --git a/api/.eslintrc.js b/api/.eslintrc.js new file mode 100644 index 00000000..fcc9e0e8 --- /dev/null +++ b/api/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true + }, + extends: [ + 'plugin:react/recommended', + 'standard-with-typescript' + ], + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json' + }, + plugins: [ + 'react' + ], + rules: { + } +} diff --git a/api/package.json b/api/package.json index a9317d68..9aabdb48 100644 --- a/api/package.json +++ b/api/package.json @@ -15,7 +15,8 @@ "watch/frontend": "NODE_ENV=development webpack watch --config ./src/frontend/webpack.config.js --mode development", "watch/legacy": "NODE_ENV=development webpack watch --config ./src/legacy/webpack.config.js --mode development", "watch/backend": "nodemon -L --watch src/backend --ext 'js,ts' src/backend/index.ts", - "debug/backend": "TS_NODE_PROJECT=./src/backend/tsconfig.json nodemon -L --watch src/backend --ext 'js,ts' --exec 'node --inspect=0.0.0.0:9229 --require ts-node/register src/backend/index.ts'" + "debug/backend": "TS_NODE_PROJECT=./src/backend/tsconfig.json nodemon -L --watch src/backend --ext 'js,ts' --exec 'node --inspect=0.0.0.0:9229 --require ts-node/register src/backend/index.ts'", + "lint": "npx eslint --fix ./src/backend & npx eslint --fix ./src/frontend" }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.44.0", @@ -59,6 +60,7 @@ "passport-jwt": "^4.0.0", "passport-openidconnect": "^0.1.1", "pg": "^8.7.1", + "prettier": "^2.8.4", "pug": "^3.0.2", "react": "^18.3.0-next-4fcc9184a-20230217", "react-bootstrap": "^2.7.2", @@ -95,7 +97,14 @@ "@types/passport-google-oauth20": "^2.0.11", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", + "@typescript-eslint/eslint-plugin": "^5.43.0", "css-loader": "^6.5.1", + "eslint": "^8.36.0", + "eslint-config-standard-with-typescript": "^34.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.32.2", "html-loader": "^3.0.1", "jest": "^29.4.3", "nodemon": "^2.0.15", @@ -105,9 +114,9 @@ "ts-loader": "^9.3.1", "ts-node": "^10.4.0", "tslint": "^6.1.3", - "typescript": "^4.5.5", + "typescript": "*", "webpack": "^5.65.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0" } -} +} \ No newline at end of file diff --git a/api/src/.prettierrc b/api/src/.prettierrc new file mode 100644 index 00000000..222861c3 --- /dev/null +++ b/api/src/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/api/src/backend/config.ts b/api/src/backend/config.ts index aa6a9799..d1779990 100644 --- a/api/src/backend/config.ts +++ b/api/src/backend/config.ts @@ -1,22 +1,22 @@ export default { - environment: process.env.NODE_ENV || "development", // development, staging, or production - port: process.env.PORT || 3000, - session: { - secret: "armavirumquecano", - saveUninitialized: false, - resave: false, + environment: process.env.NODE_ENV || "development", // development, staging, or production + port: process.env.PORT || 3000, + session: { + secret: "armavirumquecano", + saveUninitialized: false, + resave: false, + }, + oidc: { + google: { + clientID: process.env.GOOGLE_OAUTH2_CLIENT_ID, + clientSecret: process.env.GOOGLE_OAUTH2_CLIENT_SECRET, + issuer: "https://accounts.google.com", + authorizationURL: "https://accounts.google.com/o/oauth2/v2/auth", + tokenURL: "https://www.googleapis.com/oauth2/v4/token", + // FIXME: does this really need the host? Can it not just be off of "/" ? + callbackURL: `${process.env.WEB_HOST}/auth/oauth2/accounts.google.com/redirect`, }, - oidc: { - google: { - clientID: process.env.GOOGLE_OAUTH2_CLIENT_ID, - clientSecret: process.env.GOOGLE_OAUTH2_CLIENT_SECRET, - issuer: "https://accounts.google.com", - authorizationURL: "https://accounts.google.com/o/oauth2/v2/auth", - tokenURL: "https://www.googleapis.com/oauth2/v4/token", - // FIXME: does this really need the host? Can it not just be off of "/" ? - callbackURL: `${process.env.WEB_HOST}/auth/oauth2/accounts.google.com/redirect`, - } - }, - imbueNetworkWebsockAddr: process.env.IMBUE_NETWORK_WEBSOCK_ADDR, - relayChainWebsockAddr: process.env.RELAY_CHAIN_WEBSOCK_ADDR + }, + imbueNetworkWebsockAddr: process.env.IMBUE_NETWORK_WEBSOCK_ADDR, + relayChainWebsockAddr: process.env.RELAY_CHAIN_WEBSOCK_ADDR, }; diff --git a/api/src/backend/db/index.ts b/api/src/backend/db/index.ts index 475f1820..17637ea2 100644 --- a/api/src/backend/db/index.ts +++ b/api/src/backend/db/index.ts @@ -2,14 +2,15 @@ import knex from "knex"; import knexfile from "./knexfile"; import config from "../config"; - const validEnvironments = ["development", "staging", "production"]; const env = config.environment; if (!(env && validEnvironments.includes(env))) { - throw new Error( - `Must export envvar \`NODE_ENV\` as one of: "${validEnvironments.join("\", \"")}"` - ); + throw new Error( + `Must export envvar \`NODE_ENV\` as one of: "${validEnvironments.join( + '", "' + )}"` + ); } type DBEnvironment = "development" | "staging" | "production"; diff --git a/api/src/backend/db/migrations/202201141340_initial.ts b/api/src/backend/db/migrations/202201141340_initial.ts index 014a2e8e..18fb8853 100644 --- a/api/src/backend/db/migrations/202201141340_initial.ts +++ b/api/src/backend/db/migrations/202201141340_initial.ts @@ -1,232 +1,248 @@ import type { Knex } from "knex"; -import { auditFields, DROP_ON_UPDATE_TIMESTAMP_FUNCTION, onUpdateTrigger, ON_UPDATE_TIMESTAMP_FUNCTION } from "../utils"; - +import { + auditFields, + DROP_ON_UPDATE_TIMESTAMP_FUNCTION, + onUpdateTrigger, + ON_UPDATE_TIMESTAMP_FUNCTION, +} from "../utils"; export async function up(knex: Knex): Promise { - - await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION); - - const usersTableName = "users"; - await knex.schema.createTable(usersTableName, (builder) => { - /** - * We need to be able to capture users who are just casually creating - * a Project without any web3 functionality yet. So we lazily require - * the web3 stuff only when it's necessary. - */ - builder.increments("id", { primaryKey: true }); - builder.text("display_name"); - builder.integer("briefs_submitted").defaultTo(0); - builder.text("getstream_token"); - - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, usersTableName)); - - /** - * Without at least one of these, a usr can't really do much beyond saving - * a draft proposal. - */ - const web3AccountsTableName = "web3_accounts"; - await knex.schema.createTable(web3AccountsTableName, (builder) => { - builder.text("address"); - builder.integer("user_id").notNullable(); - - builder.text("type"); - builder.text("challenge"); - - builder.primary(["address"]); - builder.foreign("user_id").references("users.id"); - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, web3AccountsTableName)); - - const federatedCredsTableName = "federated_credentials"; - await knex.schema.createTable(federatedCredsTableName, (builder) => { - builder.integer("id"); - builder.text("issuer"); - builder.text("subject"); - builder.primary(["issuer", "subject"]); - - builder.foreign("id") - .references("users.id") - .onDelete("CASCADE") - .onUpdate("CASCADE"); - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, federatedCredsTableName)); - - const projectStatusTableName = "project_status"; - await knex.schema.createTable(projectStatusTableName, builder => { - builder.increments("id", { primaryKey: true }); - builder.text("status"); - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, projectStatusTableName)); - - - const projectsTableName = "projects"; - await knex.schema.createTable(projectsTableName, (builder: Knex.CreateTableBuilder) => { - builder.increments("id", { primaryKey: true }); - builder.text("name"); // project name - builder.text("logo"); // URL or dataURL (i.e., base64 encoded) - builder.text("description"); - builder.text("website"); - builder.integer("category"); - builder.integer("currency_id"); - builder.integer("chain_project_id"); - builder.decimal("total_cost_without_fee", 10, 2); - builder.decimal("imbue_fee", 10, 2); - builder.integer("status_id").notNullable().defaultTo(1); - builder.foreign("status_id") - .references("project_status.id") - .onDelete("SET NULL") - .onUpdate("CASCADE"); - - // milestones[]: `milstone` has a foreign key back to project. - - // TODO: contributions[] will probably have a foreign key back to - // project. - - // This type holds numbers as big as 1e128 and beyond (incl. fractional - // scale). The emphasis is on precision, while arithmetical efficiency - // takes a hit. - builder.decimal("required_funds", 10, 2).notNullable(); - - // FIXME: this will need to be ACID, hence we will - // need to update it from the blockchain. - // builder.decimal("withdrawn_funds").defaultTo(0); - - /** - * owner -- `AccountId` is a 32 byte array, stored as base64 encoded - * - * This shouldn't be null because we have to offer users the ability to - * submit the form without having an account, to protect their privacy. - */ - builder.text("owner"); - - /** - * This is nullable because we offer web3 account holders the ability to - * do all of their dealings wihout an account. This means that if they - * opt-out, they can't edit, etc., before finalization. - * - * This is nullable because editing is "opt-in" and we don't need an - * account, per se, to store projects. But if/when a user wants to - * create an account and associate it with a web3 address, we can update - * all of the projects whose "owner" is a `web3_account` associated with - * the `usr` account. Likewise, when a user decides to delete their - * account, we don't CASCADE in that case -- only nullify the `user_id` - * here, as it wouldn't point to anything useful. - */ - builder.integer("user_id"); - builder.foreign("user_id") - .references("users.id") - .onUpdate("CASCADE") - .onDelete("SET NULL"); - - /** - * Must be nullable; that's sort of the whole point, actually, and - * like `withdrawn_funds` we'll need to update this from the chain. - * N.B., `BlockNumber` from Substrate can be either a u32 or a byte - * array (usually represented as Hex). But it's clearly a sequential - * integer in terms of its use, so whatever we get we're going - * to want to store it as an int, so we're going with bigint here - * because postgres only has signed ints (`unsigned` is ignored). - * - * Once this is set, the project is regarded as "committed", but the - * real source of truth for this value is the chain itself. - * - * If you can't find this on the chain and this value is null, then - * the project should be considered in a "draft" state. - */ - builder.bigInteger("create_block_number").nullable(); //.unsigned(); - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, projectsTableName)); - - /** - * pub struct Milestone { - * project_key: ProjectIndex, - * milestone_index: MilestoneIndex, - * name: Vec, - * percentage_to_unlock: u32, - * is_approved: bool - * } - */ - const milestonesTableName = "milestones"; - await knex.schema.createTable(milestonesTableName, (builder) => { - builder.integer("milestone_index"); - builder.integer("project_id").notNullable(); - builder.primary(["project_id", "milestone_index"]); - builder.decimal("amount"); - - builder.foreign("project_id") - .references("projects.id") - .onDelete("CASCADE") - .onUpdate("CASCADE"); - - builder.text("name"); - builder.integer("percentage_to_unlock"); - builder.boolean("is_approved").defaultTo(false); - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, milestonesTableName)); - - /** - * TODO: ? votes and contributions - * - * It's not clear that this will ever be anything that needs to be - * stored in the database, which for now is being thought of as a kind of - * index or cache. - * - * pub struct Vote { - * yay: Balance, - * nay: Balance, - * is_approved: bool - * } - * - */ - // await knex.schema.createTable("vote", (builder) => { - // builder.bigIncrements("id"); - // builder.decimal("yay"); - // builder.decimal("nay"); - // builder.boolean("is_approved"); - - // auditFields(knex, builder); - // }); - // await knex.schema.createTable("project_vote_map", (builder) => { - // builder.integer("project_key"); - // builder.bigInteger("vote_id"); - // }); - // - // /** - // * pub struct Contribution { - // account_id: AccountId, - // value: Balance, - // } - // */ - // await knex.schema.createTable("contribution", (builder) => { - // builder.integer("idx"); - // // i.e., same as `owner` on `project` above. - // builder.string("account_id").notNullable(); - // builder.integer("project_key").notNullable(); - // builder.decimal("value").notNullable(); - // builder.primary(["project_key", "account_id", "idx"]); - - // builder.foreign("project_key") - // .references("project.key") - // .onDelete("CASCADE") - // .onUpdate("CASCADE"); - - // auditFields(knex, builder); - // }); + await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION); + + const usersTableName = "users"; + await knex.schema + .createTable(usersTableName, (builder) => { + /** + * We need to be able to capture users who are just casually creating + * a Project without any web3 functionality yet. So we lazily require + * the web3 stuff only when it's necessary. + */ + builder.increments("id", { primaryKey: true }); + builder.text("display_name"); + builder.integer("briefs_submitted").defaultTo(0); + builder.text("getstream_token"); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, usersTableName)); + + /** + * Without at least one of these, a usr can't really do much beyond saving + * a draft proposal. + */ + const web3AccountsTableName = "web3_accounts"; + await knex.schema + .createTable(web3AccountsTableName, (builder) => { + builder.text("address"); + builder.integer("user_id").notNullable(); + + builder.text("type"); + builder.text("challenge"); + + builder.primary(["address"]); + builder.foreign("user_id").references("users.id"); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, web3AccountsTableName)); + + const federatedCredsTableName = "federated_credentials"; + await knex.schema + .createTable(federatedCredsTableName, (builder) => { + builder.integer("id"); + builder.text("issuer"); + builder.text("subject"); + builder.primary(["issuer", "subject"]); + + builder + .foreign("id") + .references("users.id") + .onDelete("CASCADE") + .onUpdate("CASCADE"); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, federatedCredsTableName)); + + const projectStatusTableName = "project_status"; + await knex.schema + .createTable(projectStatusTableName, (builder) => { + builder.increments("id", { primaryKey: true }); + builder.text("status"); + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, projectStatusTableName)); + + const projectsTableName = "projects"; + await knex.schema + .createTable(projectsTableName, (builder: Knex.CreateTableBuilder) => { + builder.increments("id", { primaryKey: true }); + builder.text("name"); // project name + builder.text("logo"); // URL or dataURL (i.e., base64 encoded) + builder.text("description"); + builder.text("website"); + builder.integer("category"); + builder.integer("currency_id"); + builder.integer("chain_project_id"); + builder.decimal("total_cost_without_fee", 10, 2); + builder.decimal("imbue_fee", 10, 2); + builder.integer("status_id").notNullable().defaultTo(1); + builder + .foreign("status_id") + .references("project_status.id") + .onDelete("SET NULL") + .onUpdate("CASCADE"); + + // milestones[]: `milstone` has a foreign key back to project. + + // TODO: contributions[] will probably have a foreign key back to + // project. + + // This type holds numbers as big as 1e128 and beyond (incl. fractional + // scale). The emphasis is on precision, while arithmetical efficiency + // takes a hit. + builder.decimal("required_funds", 10, 2).notNullable(); + + // FIXME: this will need to be ACID, hence we will + // need to update it from the blockchain. + // builder.decimal("withdrawn_funds").defaultTo(0); + + /** + * owner -- `AccountId` is a 32 byte array, stored as base64 encoded + * + * This shouldn't be null because we have to offer users the ability to + * submit the form without having an account, to protect their privacy. + */ + builder.text("owner"); + + /** + * This is nullable because we offer web3 account holders the ability to + * do all of their dealings wihout an account. This means that if they + * opt-out, they can't edit, etc., before finalization. + * + * This is nullable because editing is "opt-in" and we don't need an + * account, per se, to store projects. But if/when a user wants to + * create an account and associate it with a web3 address, we can update + * all of the projects whose "owner" is a `web3_account` associated with + * the `usr` account. Likewise, when a user decides to delete their + * account, we don't CASCADE in that case -- only nullify the `user_id` + * here, as it wouldn't point to anything useful. + */ + builder.integer("user_id"); + builder + .foreign("user_id") + .references("users.id") + .onUpdate("CASCADE") + .onDelete("SET NULL"); + + /** + * Must be nullable; that's sort of the whole point, actually, and + * like `withdrawn_funds` we'll need to update this from the chain. + * N.B., `BlockNumber` from Substrate can be either a u32 or a byte + * array (usually represented as Hex). But it's clearly a sequential + * integer in terms of its use, so whatever we get we're going + * to want to store it as an int, so we're going with bigint here + * because postgres only has signed ints (`unsigned` is ignored). + * + * Once this is set, the project is regarded as "committed", but the + * real source of truth for this value is the chain itself. + * + * If you can't find this on the chain and this value is null, then + * the project should be considered in a "draft" state. + */ + builder.bigInteger("create_block_number").nullable(); //.unsigned(); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, projectsTableName)); + + /** + * pub struct Milestone { + * project_key: ProjectIndex, + * milestone_index: MilestoneIndex, + * name: Vec, + * percentage_to_unlock: u32, + * is_approved: bool + * } + */ + const milestonesTableName = "milestones"; + await knex.schema + .createTable(milestonesTableName, (builder) => { + builder.integer("milestone_index"); + builder.integer("project_id").notNullable(); + builder.primary(["project_id", "milestone_index"]); + builder.decimal("amount"); + + builder + .foreign("project_id") + .references("projects.id") + .onDelete("CASCADE") + .onUpdate("CASCADE"); + + builder.text("name"); + builder.integer("percentage_to_unlock"); + builder.boolean("is_approved").defaultTo(false); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, milestonesTableName)); + + /** + * TODO: ? votes and contributions + * + * It's not clear that this will ever be anything that needs to be + * stored in the database, which for now is being thought of as a kind of + * index or cache. + * + * pub struct Vote { + * yay: Balance, + * nay: Balance, + * is_approved: bool + * } + * + */ + // await knex.schema.createTable("vote", (builder) => { + // builder.bigIncrements("id"); + // builder.decimal("yay"); + // builder.decimal("nay"); + // builder.boolean("is_approved"); + + // auditFields(knex, builder); + // }); + // await knex.schema.createTable("project_vote_map", (builder) => { + // builder.integer("project_key"); + // builder.bigInteger("vote_id"); + // }); + // + // /** + // * pub struct Contribution { + // account_id: AccountId, + // value: Balance, + // } + // */ + // await knex.schema.createTable("contribution", (builder) => { + // builder.integer("idx"); + // // i.e., same as `owner` on `project` above. + // builder.string("account_id").notNullable(); + // builder.integer("project_key").notNullable(); + // builder.decimal("value").notNullable(); + // builder.primary(["project_key", "account_id", "idx"]); + + // builder.foreign("project_key") + // .references("project.key") + // .onDelete("CASCADE") + // .onUpdate("CASCADE"); + + // auditFields(knex, builder); + // }); } - export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("milestones"); - await knex.schema.dropTableIfExists("projects"); - await knex.schema.dropTableIfExists("project_status"); - await knex.schema.dropTableIfExists("federated_credentials"); - await knex.schema.dropTableIfExists("web3_accounts"); - await knex.schema.dropTableIfExists("users"); - await knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION); + await knex.schema.dropTableIfExists("milestones"); + await knex.schema.dropTableIfExists("projects"); + await knex.schema.dropTableIfExists("project_status"); + await knex.schema.dropTableIfExists("federated_credentials"); + await knex.schema.dropTableIfExists("web3_accounts"); + await knex.schema.dropTableIfExists("users"); + await knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION); } diff --git a/api/src/backend/db/migrations/202204201410_create_project_properties_table.ts b/api/src/backend/db/migrations/202204201410_create_project_properties_table.ts index 48b9cb79..2cb2dddb 100644 --- a/api/src/backend/db/migrations/202204201410_create_project_properties_table.ts +++ b/api/src/backend/db/migrations/202204201410_create_project_properties_table.ts @@ -1,23 +1,21 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; - export async function up(knex: Knex): Promise { - const tableName = "project_properties"; - await knex.schema.createTable(tableName, (builder) => { - builder.increments("id", { primaryKey: true }); - builder.text("faq"); - builder.integer("project_id").notNullable(); + const tableName = "project_properties"; + await knex.schema + .createTable(tableName, (builder) => { + builder.increments("id", { primaryKey: true }); + builder.text("faq"); + builder.integer("project_id").notNullable(); - builder.foreign("project_id") - .references("projects.id"); + builder.foreign("project_id").references("projects.id"); - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, tableName)); + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, tableName)); } - export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("project_properties"); + await knex.schema.dropTableIfExists("project_properties"); } - diff --git a/api/src/backend/db/migrations/202210061410_create_brief.ts b/api/src/backend/db/migrations/202210061410_create_brief.ts index 7fc88df0..40b4d0c9 100644 --- a/api/src/backend/db/migrations/202210061410_create_brief.ts +++ b/api/src/backend/db/migrations/202210061410_create_brief.ts @@ -1,39 +1,40 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; - export async function up(knex: Knex): Promise { - const tableName = "briefs"; - await knex.schema.createTable(tableName, (builder) => { - builder.increments("id", { primaryKey: true }); - builder.text("headline"); + const tableName = "briefs"; + await knex.schema + .createTable(tableName, (builder) => { + builder.increments("id", { primaryKey: true }); + builder.text("headline"); - builder.text("description"); - builder.integer("scope_id"); - builder.integer("duration_id"); - builder.bigInteger("budget"); - builder.integer("experience_id"); - builder.integer("project_id"); - builder.foreign("project_id").references("projects.id"); + builder.text("description"); + builder.integer("scope_id"); + builder.integer("duration_id"); + builder.bigInteger("budget"); + builder.integer("experience_id"); + builder.integer("project_id"); + builder.foreign("project_id").references("projects.id"); - // stored in its own table - // The foreign key is put on in the experience migration. - builder.integer("user_id"); - builder.foreign("user_id").references("users.id"); - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, tableName)).then(function () { - return knex.schema.alterTable("projects", (table) => { - table.integer("brief_id"); - }) - }).then(function () { - return knex.schema.alterTable("projects", (table) => { - table.foreign("brief_id").references("briefs.id"); - }) + // stored in its own table + // The foreign key is put on in the experience migration. + builder.integer("user_id"); + builder.foreign("user_id").references("users.id"); + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, tableName)) + .then(function () { + return knex.schema.alterTable("projects", (table) => { + table.integer("brief_id"); + }); + }) + .then(function () { + return knex.schema.alterTable("projects", (table) => { + table.foreign("brief_id").references("briefs.id"); + }); }); } - export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("briefs"); + await knex.schema.dropTableIfExists("briefs"); } - diff --git a/api/src/backend/db/migrations/202210241002_milestone.ts b/api/src/backend/db/migrations/202210241002_milestone.ts index 9a4d68bd..934fd8f9 100644 --- a/api/src/backend/db/migrations/202210241002_milestone.ts +++ b/api/src/backend/db/migrations/202210241002_milestone.ts @@ -5,21 +5,23 @@ import { auditFields, onUpdateTrigger } from "../utils"; ** New table to store the milestone details */ export async function up(knex: Knex): Promise { - const milestoneDetailsTableName = "milestone_details"; - await knex.schema.createTable(milestoneDetailsTableName, (builder) => { - builder.integer("index").notNullable(); - builder.integer("project_id").notNullable(); - builder.string("details").nullable(); - builder.primary(["project_id", "index"]); - builder.foreign("project_id") - .references("projects.id") - .onDelete("CASCADE") - .onUpdate("CASCADE"); - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, milestoneDetailsTableName)); + const milestoneDetailsTableName = "milestone_details"; + await knex.schema + .createTable(milestoneDetailsTableName, (builder) => { + builder.integer("index").notNullable(); + builder.integer("project_id").notNullable(); + builder.string("details").nullable(); + builder.primary(["project_id", "index"]); + builder + .foreign("project_id") + .references("projects.id") + .onDelete("CASCADE") + .onUpdate("CASCADE"); + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, milestoneDetailsTableName)); } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("milestone_details"); + await knex.schema.dropTableIfExists("milestone_details"); } - diff --git a/api/src/backend/db/migrations/202301071344_freelancers.ts b/api/src/backend/db/migrations/202301071344_freelancers.ts index 97341a5a..ef28413f 100644 --- a/api/src/backend/db/migrations/202301071344_freelancers.ts +++ b/api/src/backend/db/migrations/202301071344_freelancers.ts @@ -1,42 +1,41 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; - export async function up(knex: Knex): Promise { - const tableName = "freelancers"; - await knex.schema.createTable(tableName, (builder) => { - builder.increments("id", { primaryKey: true }); - builder.text("freelanced_before"); - builder.text("freelancing_goal"); - builder.text("work_type"); - builder.text("education"); - builder.text("experience"); - builder.text("title"); - - builder.text("bio"); - - // These fields can be found in tables: - // freelancer_language, freelancer_client, freelancer_services, freelancer_skills - - //builder.specificType("language_ids", "integer[]"); - //builder.specificType("client_ids", "integer[]"); - //builder.specificType("services_ids", "integer[]"); - //builder.specificType("skill_ids", "integer[]"); - - builder.text("facebook_link"); - builder.text("twitter_link"); - builder.text("telegram_link"); - builder.text("discord_link"); - - builder.integer("user_id"); - builder.foreign("user_id") - .references("users.id"); - - auditFields(knex, builder); - }).then(onUpdateTrigger(knex, tableName)); + const tableName = "freelancers"; + await knex.schema + .createTable(tableName, (builder) => { + builder.increments("id", { primaryKey: true }); + builder.text("freelanced_before"); + builder.text("freelancing_goal"); + builder.text("work_type"); + builder.text("education"); + builder.text("experience"); + builder.text("title"); + + builder.text("bio"); + + // These fields can be found in tables: + // freelancer_language, freelancer_client, freelancer_services, freelancer_skills + + //builder.specificType("language_ids", "integer[]"); + //builder.specificType("client_ids", "integer[]"); + //builder.specificType("services_ids", "integer[]"); + //builder.specificType("skill_ids", "integer[]"); + + builder.text("facebook_link"); + builder.text("twitter_link"); + builder.text("telegram_link"); + builder.text("discord_link"); + + builder.integer("user_id"); + builder.foreign("user_id").references("users.id"); + + auditFields(knex, builder); + }) + .then(onUpdateTrigger(knex, tableName)); } - export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("freelancers"); + await knex.schema.dropTableIfExists("freelancers"); } diff --git a/api/src/backend/db/migrations/202301161145_experience_and_scope_and_duration.ts b/api/src/backend/db/migrations/202301161145_experience_and_scope_and_duration.ts index 50dd930e..d5e27d0b 100644 --- a/api/src/backend/db/migrations/202301161145_experience_and_scope_and_duration.ts +++ b/api/src/backend/db/migrations/202301161145_experience_and_scope_and_duration.ts @@ -2,42 +2,45 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - await knex.schema.createTable("experience", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.string("experience_level"); - auditFields(knex, builder); - - }).then(function() { - return knex.schema.alterTable("briefs", (table) => { - table.foreign("experience_id").references("experience.id"); - }) + await knex.schema + .createTable("experience", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("experience_level"); + auditFields(knex, builder); + }) + .then(function () { + return knex.schema.alterTable("briefs", (table) => { + table.foreign("experience_id").references("experience.id"); + }); }); - await knex.schema.createTable("scope", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.string("scope_level"); - auditFields(knex, builder); - - }).then(function() { - return knex.schema.alterTable("briefs", (table) => { - table.foreign("scope_id").references("scope.id"); - }) + await knex.schema + .createTable("scope", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("scope_level"); + auditFields(knex, builder); + }) + .then(function () { + return knex.schema.alterTable("briefs", (table) => { + table.foreign("scope_id").references("scope.id"); + }); }); - await knex.schema.createTable("duration", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.string("duration"); - auditFields(knex, builder); - - }).then(function() { - return knex.schema.alterTable("briefs", (table) => { - table.foreign("duration_id").references("duration.id"); - }) + await knex.schema + .createTable("duration", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("duration"); + auditFields(knex, builder); + }) + .then(function () { + return knex.schema.alterTable("briefs", (table) => { + table.foreign("duration_id").references("duration.id"); + }); }); -}; +} export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("experience"); - await knex.schema.dropTableIfExists("scope"); - await knex.schema.dropTableIfExists("duration"); -}; + await knex.schema.dropTableIfExists("experience"); + await knex.schema.dropTableIfExists("scope"); + await knex.schema.dropTableIfExists("duration"); +} diff --git a/api/src/backend/db/migrations/202301171002_user_logins.ts b/api/src/backend/db/migrations/202301171002_user_logins.ts index 26f44ce0..b12c8daf 100644 --- a/api/src/backend/db/migrations/202301171002_user_logins.ts +++ b/api/src/backend/db/migrations/202301171002_user_logins.ts @@ -1,22 +1,27 @@ import type { Knex } from "knex"; -import { auditFields, DROP_ON_UPDATE_TIMESTAMP_FUNCTION, onUpdateTrigger, ON_UPDATE_TIMESTAMP_FUNCTION } from "../utils"; +import { + auditFields, + DROP_ON_UPDATE_TIMESTAMP_FUNCTION, + onUpdateTrigger, + ON_UPDATE_TIMESTAMP_FUNCTION, +} from "../utils"; export async function up(knex: Knex): Promise { - await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION); - const usersTableName = "users"; - await knex.schema.alterTable(usersTableName, (builder) => { - // username must be unique. - builder.text("username").unique(); - builder.text("email"); - builder.text("password"); - }); + await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION); + const usersTableName = "users"; + await knex.schema.alterTable(usersTableName, (builder) => { + // username must be unique. + builder.text("username").unique(); + builder.text("email"); + builder.text("password"); + }); } export async function down(knex: Knex): Promise { - const usersTableName = "users"; - await knex.schema.alterTable(usersTableName, (builder) => { - builder.dropColumn("username"); - builder.dropColumn("email"); - builder.dropColumn("password"); - }); -} \ No newline at end of file + const usersTableName = "users"; + await knex.schema.alterTable(usersTableName, (builder) => { + builder.dropColumn("username"); + builder.dropColumn("email"); + builder.dropColumn("password"); + }); +} diff --git a/api/src/backend/db/migrations/202301301622_skills_and_industries.ts b/api/src/backend/db/migrations/202301301622_skills_and_industries.ts index febab4d6..0492a773 100644 --- a/api/src/backend/db/migrations/202301301622_skills_and_industries.ts +++ b/api/src/backend/db/migrations/202301301622_skills_and_industries.ts @@ -2,52 +2,53 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - - // Base Tables - await knex.schema.createTable("skills", (builder) => { + // Base Tables + await knex.schema + .createTable("skills", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("name"); + auditFields(knex, builder); + }) + .then(async () => { + await knex.schema.createTable("freelancer_skills", (builder) => { builder.increments("id", { primaryKey: true }); - builder.string("name"); + builder.integer("skill_id"); + builder.integer("freelancer_id"); + builder.foreign("skill_id").references("skills.id"); + builder.foreign("freelancer_id").references("freelancers.id"); auditFields(knex, builder); - }).then( async () => { - await knex.schema.createTable("freelancer_skills", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("skill_id"); - builder.integer("freelancer_id"); - builder.foreign("skill_id").references("skills.id"); - builder.foreign("freelancer_id").references("freelancers.id"); - auditFields(knex, builder); - }); - await knex.schema.createTable("brief_skills", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("skill_id"); - builder.integer("brief_id"); - builder.foreign("skill_id").references("skills.id"); - builder.foreign("brief_id").references("briefs.id"); - auditFields(knex, builder); - }); + }); + await knex.schema.createTable("brief_skills", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.integer("skill_id"); + builder.integer("brief_id"); + builder.foreign("skill_id").references("skills.id"); + builder.foreign("brief_id").references("briefs.id"); + auditFields(knex, builder); + }); }); - await knex.schema.createTable("industries", (builder) => { + await knex.schema + .createTable("industries", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("name"); + auditFields(knex, builder); + }) + .then(async () => { + await knex.schema.createTable("brief_industries", (builder) => { builder.increments("id", { primaryKey: true }); - builder.string("name"); + builder.integer("industry_id"); + builder.integer("brief_id"); + builder.foreign("industry_id").references("industries.id"); + builder.foreign("brief_id").references("briefs.id"); auditFields(knex, builder); - }).then(async () => { - await knex.schema.createTable("brief_industries", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("industry_id"); - builder.integer("brief_id"); - builder.foreign("industry_id").references("industries.id"); - builder.foreign("brief_id").references("briefs.id"); - auditFields(knex, builder); - }); + }); }); - - // Associated many-many tables - -}; + // Associated many-many tables +} export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("skills"); - await knex.schema.dropTableIfExists("industries"); -}; + await knex.schema.dropTableIfExists("skills"); + await knex.schema.dropTableIfExists("industries"); +} diff --git a/api/src/backend/db/migrations/202301310120_clients_and_services_offered_and_languages.ts b/api/src/backend/db/migrations/202301310120_clients_and_services_offered_and_languages.ts index 2da1b181..a84473c8 100644 --- a/api/src/backend/db/migrations/202301310120_clients_and_services_offered_and_languages.ts +++ b/api/src/backend/db/migrations/202301310120_clients_and_services_offered_and_languages.ts @@ -2,65 +2,66 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - - //builder.specificType("client_ids", "integer[]"); - await knex.schema.createTable("clients", (builder) => { + //builder.specificType("client_ids", "integer[]"); + await knex.schema + .createTable("clients", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("name"); + builder.string("img"); + auditFields(knex, builder); + }) + .then(async () => { + await knex.schema.createTable("freelancer_clients", (builder) => { builder.increments("id", { primaryKey: true }); - builder.string("name"); - builder.string("img"); + builder.integer("client_id"); + builder.integer("freelancer_id"); + builder.foreign("client_id").references("clients.id"); + builder.foreign("freelancer_id").references("freelancers.id"); auditFields(knex, builder); - }).then(async () => { - await knex.schema.createTable("freelancer_clients", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("client_id"); - builder.integer("freelancer_id"); - builder.foreign("client_id").references("clients.id"); - builder.foreign("freelancer_id").references("freelancers.id"); - auditFields(knex, builder); - }); - }); + }); + }); - //builder.specificType("services_ids", "integer[]"); - await knex.schema.createTable("services", (builder) => { + //builder.specificType("services_ids", "integer[]"); + await knex.schema + .createTable("services", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("name"); + auditFields(knex, builder); + }) + .then(async () => { + await knex.schema.createTable("freelancer_services", (builder) => { builder.increments("id", { primaryKey: true }); - builder.string("name"); + builder.integer("service_id"); + builder.integer("freelancer_id"); + builder.foreign("service_id").references("services.id"); + builder.foreign("freelancer_id").references("freelancers.id"); auditFields(knex, builder); - }).then(async () => { - await knex.schema.createTable("freelancer_services", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("service_id"); - builder.integer("freelancer_id"); - builder.foreign("service_id").references("services.id"); - builder.foreign("freelancer_id").references("freelancers.id"); - auditFields(knex, builder); - }); - - }); + }); + }); - //builder.specificType("language_ids", "integer[]"); - await knex.schema.createTable("languages", (builder) => { + //builder.specificType("language_ids", "integer[]"); + await knex.schema + .createTable("languages", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.string("name"); + auditFields(knex, builder); + }) + .then(async () => { + await knex.schema.createTable("freelancer_languages", (builder) => { builder.increments("id", { primaryKey: true }); - builder.string("name"); + builder.integer("language_id"); + builder.integer("freelancer_id"); + builder.foreign("language_id").references("languages.id"); + builder.foreign("freelancer_id").references("freelancers.id"); auditFields(knex, builder); - }).then(async () => { - await knex.schema.createTable("freelancer_languages", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("language_id"); - builder.integer("freelancer_id"); - builder.foreign("language_id").references("languages.id"); - builder.foreign("freelancer_id").references("freelancers.id"); - auditFields(knex, builder); - }); - }); - - -}; - - //builder.specificType("skill_ids", "integer[]"); + }); + }); +} +//builder.specificType("skill_ids", "integer[]"); export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("clients"); - await knex.schema.dropTableIfExists("services"); - await knex.schema.dropTableIfExists("languages"); -}; + await knex.schema.dropTableIfExists("clients"); + await knex.schema.dropTableIfExists("services"); + await knex.schema.dropTableIfExists("languages"); +} diff --git a/api/src/backend/db/migrations/20230210132754_freelancer_ratings.ts b/api/src/backend/db/migrations/20230210132754_freelancer_ratings.ts index 7ed5c32b..c4d15418 100644 --- a/api/src/backend/db/migrations/20230210132754_freelancer_ratings.ts +++ b/api/src/backend/db/migrations/20230210132754_freelancer_ratings.ts @@ -2,16 +2,15 @@ import { Knex } from "knex"; import { auditFields, onUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - await knex.schema.createTable("freelancer_ratings", (builder) => { - builder.increments("id", { primaryKey: true }); - builder.integer("freelancer_id"); - builder.foreign("freelancer_id").references("freelancers.id"); - builder.integer("rating"); - auditFields(knex, builder); - } - ) -}; + await knex.schema.createTable("freelancer_ratings", (builder) => { + builder.increments("id", { primaryKey: true }); + builder.integer("freelancer_id"); + builder.foreign("freelancer_id").references("freelancers.id"); + builder.integer("rating"); + auditFields(knex, builder); + }); +} export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("freelancer_ratings"); -}; + await knex.schema.dropTableIfExists("freelancer_ratings"); +} diff --git a/api/src/backend/db/seeds/202301301600_ delete.ts b/api/src/backend/db/seeds/202301301600_ delete.ts index 7be4e8aa..3a6c7095 100644 --- a/api/src/backend/db/seeds/202301301600_ delete.ts +++ b/api/src/backend/db/seeds/202301301600_ delete.ts @@ -1,17 +1,17 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("brief_skills").del(); - await knex("brief_industries").del(); - await knex("briefs").del(); - await knex("freelancer_services").del(); - await knex("freelancer_clients").del(); - await knex("freelancer_skills").del(); - await knex("freelancer_languages").del(); - await knex("freelancers").del(); - await knex("users").del(); - await knex("project_status").del(); - await knex("experience").del(); - await knex("skills").del(); - await knex("industries").del(); -}; + await knex("brief_skills").del(); + await knex("brief_industries").del(); + await knex("briefs").del(); + await knex("freelancer_services").del(); + await knex("freelancer_clients").del(); + await knex("freelancer_skills").del(); + await knex("freelancer_languages").del(); + await knex("freelancers").del(); + await knex("users").del(); + await knex("project_status").del(); + await knex("experience").del(); + await knex("skills").del(); + await knex("industries").del(); +} diff --git a/api/src/backend/db/seeds/202301301601_project_statuses.ts b/api/src/backend/db/seeds/202301301601_project_statuses.ts index 687d9bf8..d975215c 100644 --- a/api/src/backend/db/seeds/202301301601_project_statuses.ts +++ b/api/src/backend/db/seeds/202301301601_project_statuses.ts @@ -1,10 +1,10 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("project_status").insert([ - { status: "draft" }, - { status: "pending review" }, - { status: "rejected" }, - { status: "accepted" }, - ]); -}; + await knex("project_status").insert([ + { status: "draft" }, + { status: "pending review" }, + { status: "rejected" }, + { status: "accepted" }, + ]); +} diff --git a/api/src/backend/db/seeds/202301301602_experience_and_scope_and_duration.ts b/api/src/backend/db/seeds/202301301602_experience_and_scope_and_duration.ts index d27a803c..8bfbf807 100644 --- a/api/src/backend/db/seeds/202301301602_experience_and_scope_and_duration.ts +++ b/api/src/backend/db/seeds/202301301602_experience_and_scope_and_duration.ts @@ -1,24 +1,24 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("experience").insert([ - {experience_level: "Entry Level"}, - {experience_level: "Intermediate"}, - {experience_level: "Expert"}, - {experience_level: "Specialist"}, - ]); + await knex("experience").insert([ + { experience_level: "Entry Level" }, + { experience_level: "Intermediate" }, + { experience_level: "Expert" }, + { experience_level: "Specialist" }, + ]); - await knex("scope").insert([ - {scope_level: "Small"}, - {scope_level: "Medium"}, - {scope_level: "Large"}, - {scope_level: "Complex"}, - ]); + await knex("scope").insert([ + { scope_level: "Small" }, + { scope_level: "Medium" }, + { scope_level: "Large" }, + { scope_level: "Complex" }, + ]); - await knex("duration").insert([ - {duration: "1 to 3 months"}, - {duration: "3-6 months"}, - {duration: "More than 6 months"}, - {duration: "More than a year"}, - ]); -}; + await knex("duration").insert([ + { duration: "1 to 3 months" }, + { duration: "3-6 months" }, + { duration: "More than 6 months" }, + { duration: "More than a year" }, + ]); +} diff --git a/api/src/backend/db/seeds/202301301603_users_test.ts b/api/src/backend/db/seeds/202301301603_users_test.ts index 22ea755d..0c89b6f4 100644 --- a/api/src/backend/db/seeds/202301301603_users_test.ts +++ b/api/src/backend/db/seeds/202301301603_users_test.ts @@ -1,33 +1,32 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("users").insert([ - { - display_name: "Dude Mckenzie", - username: "dudester", - email: "dudemks@gmail.com", - password: "testpassword", - briefs_submitted: 20, - }, - { - display_name: "Mike Doomer", - username: "doober", - email: "mdoomer@gmail.com", - password: "testpassword", - briefs_submitted: 3, - }, - { - display_name: "Web3 dev", - username: "web3dev", - email: "web3dev@gmail.com", - password: "testpassword", - }, - { - display_name: "frontend dev", - username: "frontenddev", - email: "frontenddev@gmail.com", - password: "testpassword", - } - ]); -}; - + await knex("users").insert([ + { + display_name: "Dude Mckenzie", + username: "dudester", + email: "dudemks@gmail.com", + password: "testpassword", + briefs_submitted: 20, + }, + { + display_name: "Mike Doomer", + username: "doober", + email: "mdoomer@gmail.com", + password: "testpassword", + briefs_submitted: 3, + }, + { + display_name: "Web3 dev", + username: "web3dev", + email: "web3dev@gmail.com", + password: "testpassword", + }, + { + display_name: "frontend dev", + username: "frontenddev", + email: "frontenddev@gmail.com", + password: "testpassword", + }, + ]); +} diff --git a/api/src/backend/db/seeds/202301301604_briefs_test.ts b/api/src/backend/db/seeds/202301301604_briefs_test.ts index b405bb31..27e4b44e 100644 --- a/api/src/backend/db/seeds/202301301604_briefs_test.ts +++ b/api/src/backend/db/seeds/202301301604_briefs_test.ts @@ -1,28 +1,30 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("briefs").insert([ + await knex("briefs").insert([ { - headline: "Amazing Frontend Developer NEEDED!!", - //industry_ids: [1, 2], - description: "We need some absolute wizard to create an ultra dynamic thing with carosels", - duration_id: 2, - budget: 1000, - experience_id: 2, - user_id: 1, - scope_id: 2, - //skill_ids: [3,4] + headline: "Amazing Frontend Developer NEEDED!!", + //industry_ids: [1, 2], + description: + "We need some absolute wizard to create an ultra dynamic thing with carosels", + duration_id: 2, + budget: 1000, + experience_id: 2, + user_id: 1, + scope_id: 2, + //skill_ids: [3,4] }, { - headline: "Amazing C++ Developer", - //industry_ids: [2, 3], - description: "We need some absolute wizard to create an ultra cool, mega thing with spinning balls", - duration_id: 1, - budget: 200, - experience_id: 3, - user_id: 2, - scope_id: 1, - //skill_ids: [2,6] - } - ]); -}; + headline: "Amazing C++ Developer", + //industry_ids: [2, 3], + description: + "We need some absolute wizard to create an ultra cool, mega thing with spinning balls", + duration_id: 1, + budget: 200, + experience_id: 3, + user_id: 2, + scope_id: 1, + //skill_ids: [2,6] + }, + ]); +} diff --git a/api/src/backend/db/seeds/202301301605_skills_and_industries.ts b/api/src/backend/db/seeds/202301301605_skills_and_industries.ts index 5613cacf..4dfc8758 100644 --- a/api/src/backend/db/seeds/202301301605_skills_and_industries.ts +++ b/api/src/backend/db/seeds/202301301605_skills_and_industries.ts @@ -1,48 +1,47 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("skills").insert([ - { name: "substrate" }, - { name: "rust" }, - { name: "react" }, - { name: "typescript" }, - { name: "javascript" }, - { name: "c++" }, - { name: "polkadot" }, - { name: "figma" }, - { name: "adobe photoshop" }, - ]); + await knex("skills").insert([ + { name: "substrate" }, + { name: "rust" }, + { name: "react" }, + { name: "typescript" }, + { name: "javascript" }, + { name: "c++" }, + { name: "polkadot" }, + { name: "figma" }, + { name: "adobe photoshop" }, + ]); - await knex("industries").insert([ - { name: "web3" }, - { name: "defi" }, - { name: "education" }, - { name: "agriculture" }, - { name: "communications" }, - { name: "health" }, - { name: "wellness" }, - { name: "energy" }, - { name: "sustainability" }, - { name: "arts and culture" }, - { name: "real estate" }, - { name: "technology" }, - { name: "supply chain" }, - ]); + await knex("industries").insert([ + { name: "web3" }, + { name: "defi" }, + { name: "education" }, + { name: "agriculture" }, + { name: "communications" }, + { name: "health" }, + { name: "wellness" }, + { name: "energy" }, + { name: "sustainability" }, + { name: "arts and culture" }, + { name: "real estate" }, + { name: "technology" }, + { name: "supply chain" }, + ]); - // Backend Brief - await knex("brief_skills").insert({ brief_id: 1, skill_id: 1 }); - await knex("brief_skills").insert({ brief_id: 1, skill_id: 2 }); - await knex("brief_skills").insert({ brief_id: 1, skill_id: 6 }); - await knex("brief_skills").insert({ brief_id: 1, skill_id: 7 }); - await knex("brief_industries").insert({ brief_id: 1, industry_id: 1 }); - await knex("brief_industries").insert({ brief_id: 1, industry_id: 2 }); + // Backend Brief + await knex("brief_skills").insert({ brief_id: 1, skill_id: 1 }); + await knex("brief_skills").insert({ brief_id: 1, skill_id: 2 }); + await knex("brief_skills").insert({ brief_id: 1, skill_id: 6 }); + await knex("brief_skills").insert({ brief_id: 1, skill_id: 7 }); + await knex("brief_industries").insert({ brief_id: 1, industry_id: 1 }); + await knex("brief_industries").insert({ brief_id: 1, industry_id: 2 }); - // Frontend Brief - await knex("brief_skills").insert({ brief_id: 2, skill_id: 3 }); - await knex("brief_skills").insert({ brief_id: 2, skill_id: 4 }); - await knex("brief_skills").insert({ brief_id: 2, skill_id: 5 }); - await knex("brief_skills").insert({ brief_id: 2, skill_id: 8 }); - await knex("brief_industries").insert({ brief_id: 2, industry_id: 1 }); - await knex("brief_industries").insert({ brief_id: 2, industry_id: 2 }); - -}; + // Frontend Brief + await knex("brief_skills").insert({ brief_id: 2, skill_id: 3 }); + await knex("brief_skills").insert({ brief_id: 2, skill_id: 4 }); + await knex("brief_skills").insert({ brief_id: 2, skill_id: 5 }); + await knex("brief_skills").insert({ brief_id: 2, skill_id: 8 }); + await knex("brief_industries").insert({ brief_id: 2, industry_id: 1 }); + await knex("brief_industries").insert({ brief_id: 2, industry_id: 2 }); +} diff --git a/api/src/backend/db/seeds/202301311299_clients_and_services_offered_and_languages.ts b/api/src/backend/db/seeds/202301311299_clients_and_services_offered_and_languages.ts index 1004fe7a..157dc37a 100644 --- a/api/src/backend/db/seeds/202301311299_clients_and_services_offered_and_languages.ts +++ b/api/src/backend/db/seeds/202301311299_clients_and_services_offered_and_languages.ts @@ -1,30 +1,30 @@ import { Knex } from "knex"; export async function seed(knex: Knex): Promise { - await knex("services").insert([ - {name: "web development"}, - {name: "web design"}, - {name: "mobile (android/ios)"}, - {name: "copywriting"}, - {name: "smart contracts"}, - {name: "video editing"}, - {name: "nft"}, - ]); + await knex("services").insert([ + { name: "web development" }, + { name: "web design" }, + { name: "mobile (android/ios)" }, + { name: "copywriting" }, + { name: "smart contracts" }, + { name: "video editing" }, + { name: "nft" }, + ]); - await knex("clients").insert([ - {name: "imbue", img:"imbue-img"}, - {name: "mangata", img:"mangata-img"}, - {name: "oak", img:"oak-img"}, - {name: "nft club", img:"nft-club-img"}, - ]); + await knex("clients").insert([ + { name: "imbue", img: "imbue-img" }, + { name: "mangata", img: "mangata-img" }, + { name: "oak", img: "oak-img" }, + { name: "nft club", img: "nft-club-img" }, + ]); - await knex("languages").insert([ - {name: "french"}, - {name: "german"}, - {name: "dutch"}, - {name: "spanish"}, - {name: "arabic"}, - {name: "urdu"}, - {name: "hindi"}, - ]); -}; + await knex("languages").insert([ + { name: "french" }, + { name: "german" }, + { name: "dutch" }, + { name: "spanish" }, + { name: "arabic" }, + { name: "urdu" }, + { name: "hindi" }, + ]); +} diff --git a/api/src/backend/db/seeds/202301311300_freelancers.ts b/api/src/backend/db/seeds/202301311300_freelancers.ts index b91f4c61..1f386d98 100644 --- a/api/src/backend/db/seeds/202301311300_freelancers.ts +++ b/api/src/backend/db/seeds/202301311300_freelancers.ts @@ -1,104 +1,143 @@ import { Knex } from "knex"; -import bcrypt from 'bcryptjs' +import bcrypt from "bcryptjs"; export async function seed(knex: Knex): Promise { - let skills = [ - "substrate", - "rust", - "react", - "typescript", - "javascript", - "c", - "polkadot", - "figma", - "adobe", - ] - let services = [ - "web development", - "web design", - "mobile", - "android", - "copywriting", - "smart contracts", - "video editing", - "nft", - ] - let clients = [ - "imbue", - "mangata", - "oak", - "nft", - ] - let languages = [ - "french", - "german", - "dutch", - "spanish", - "arabic", - "urdu", - "hindi", - ] - let id = 0; - for (let a = 1; a < skills.length; a++) { - for (let b = 1; b < clients.length; b++) { - for (let c = 1; c < languages.length; c++) { - for (let d = 1; d < services.length; d++) { - id = id + 1; - let rsmall = Math.floor(Math.random() * (5)); - let rbig = Math.floor(Math.random() * (100000)); + let skills = [ + "substrate", + "rust", + "react", + "typescript", + "javascript", + "c", + "polkadot", + "figma", + "adobe", + ]; + let services = [ + "web development", + "web design", + "mobile", + "android", + "copywriting", + "smart contracts", + "video editing", + "nft", + ]; + let clients = ["imbue", "mangata", "oak", "nft"]; + let languages = [ + "french", + "german", + "dutch", + "spanish", + "arabic", + "urdu", + "hindi", + ]; + let id = 0; + for (let a = 1; a < skills.length; a++) { + for (let b = 1; b < clients.length; b++) { + for (let c = 1; c < languages.length; c++) { + for (let d = 1; d < services.length; d++) { + id = id + 1; + let rsmall = Math.floor(Math.random() * 5); + let rbig = Math.floor(Math.random() * 100000); - // new user associated - await knex("users").insert( - [{ - display_name: skills[a] + "_" + clients[b] + "_" + services[d] + rbig, - username: skills[a].replace(" ","_") + "_" + clients[b].replace(" ","_") + "_" + services[d].replace(" ","_") + rbig, - email: skills[a] + languages[c] + rbig + "@gmail.com", - password: a < 2 ? bcrypt.hashSync("testpassword") : "testpassword" - }] - ).then(async () => { - await knex("freelancers").insert({ - freelanced_before: "I've freelanced before however, i may need some extra help.", - freelancing_goal: "To make a little extra money on the side", - title: "Mega cool " + skills[a] + " professional " + rbig, - bio: "I also have experience in " + services[d] + " and have many clients including " + clients[b], - work_type: "To make a little extra money on the side", - facebook_link: "www.facebook.com/pro" + skills[a], - twitter_link: "www.twitter.com/pro" + skills[a], - telegram_link: "www.telegram.com/pro" + skills[a], - discord_link: "www.discord.com/pro" + skills[a], - user_id: id + 3, - }).then(async () => { - - for (let i = 0; i <= rsmall; i++) { - - if (i == 0) { - await knex("freelancer_skills").insert({ freelancer_id: id, skill_id: a}) - await knex("freelancer_skills").insert({ freelancer_id: id, skill_id: b}) - await knex("freelancer_skills").insert({ freelancer_id: id, skill_id: c}) - await knex("freelancer_clients").insert({freelancer_id: id, client_id: b}) - await knex("freelancer_languages").insert({freelancer_id: id,language_id: c}) - await knex("freelancer_services").insert({freelancer_id: id, service_id: d}) - if (i > 0) { - if (i != a) { - await knex("freelancer_skills").insert({ freelancer_id: id, skill_id: i}) - } - if (i != b) { - await knex("freelancer_clients").insert({ freelancer_id: id, client_id: i}) - } - if (i != c) { - await knex("freelancer_languages").insert({ freelancer_id: id, language_id: i}) - } - if (i != d) { - await knex("freelancer_services").insert({ freelancer_id: id, service_id: i}) - } - } - } - }}) - }) - } - } + // new user associated + await knex("users") + .insert([ + { + display_name: + skills[a] + "_" + clients[b] + "_" + services[d] + rbig, + username: + skills[a].replace(" ", "_") + + "_" + + clients[b].replace(" ", "_") + + "_" + + services[d].replace(" ", "_") + + rbig, + email: skills[a] + languages[c] + rbig + "@gmail.com", + password: + a < 2 ? bcrypt.hashSync("testpassword") : "testpassword", + }, + ]) + .then(async () => { + await knex("freelancers") + .insert({ + freelanced_before: + "I've freelanced before however, i may need some extra help.", + freelancing_goal: "To make a little extra money on the side", + title: "Mega cool " + skills[a] + " professional " + rbig, + bio: + "I also have experience in " + + services[d] + + " and have many clients including " + + clients[b], + work_type: "To make a little extra money on the side", + facebook_link: "www.facebook.com/pro" + skills[a], + twitter_link: "www.twitter.com/pro" + skills[a], + telegram_link: "www.telegram.com/pro" + skills[a], + discord_link: "www.discord.com/pro" + skills[a], + user_id: id + 3, + }) + .then(async () => { + for (let i = 0; i <= rsmall; i++) { + if (i == 0) { + await knex("freelancer_skills").insert({ + freelancer_id: id, + skill_id: a, + }); + await knex("freelancer_skills").insert({ + freelancer_id: id, + skill_id: b, + }); + await knex("freelancer_skills").insert({ + freelancer_id: id, + skill_id: c, + }); + await knex("freelancer_clients").insert({ + freelancer_id: id, + client_id: b, + }); + await knex("freelancer_languages").insert({ + freelancer_id: id, + language_id: c, + }); + await knex("freelancer_services").insert({ + freelancer_id: id, + service_id: d, + }); + if (i > 0) { + if (i != a) { + await knex("freelancer_skills").insert({ + freelancer_id: id, + skill_id: i, + }); + } + if (i != b) { + await knex("freelancer_clients").insert({ + freelancer_id: id, + client_id: i, + }); + } + if (i != c) { + await knex("freelancer_languages").insert({ + freelancer_id: id, + language_id: i, + }); + } + if (i != d) { + await knex("freelancer_services").insert({ + freelancer_id: id, + service_id: i, + }); + } + } + } + } + }); + }); } + } } - -}; - \ No newline at end of file + } +} diff --git a/api/src/backend/db/utils.ts b/api/src/backend/db/utils.ts index 4612509d..14c5a179 100644 --- a/api/src/backend/db/utils.ts +++ b/api/src/backend/db/utils.ts @@ -11,18 +11,18 @@ END; $$ language 'plpgsql'; `; export const DROP_ON_UPDATE_TIMESTAMP_FUNCTION = - "DROP FUNCTION on_update_timestamp"; + "DROP FUNCTION on_update_timestamp"; export const current_timestamp = (knex: Knex) => knex.raw(CTATZU); export const auditFields = (knex: Knex, builder: Knex.CreateTableBuilder) => { - builder.datetime("created").defaultTo(current_timestamp(knex)); - builder.datetime("modified").defaultTo(current_timestamp(knex)); + builder.datetime("created").defaultTo(current_timestamp(knex)); + builder.datetime("modified").defaultTo(current_timestamp(knex)); }; -export const onUpdateTrigger = (knex: Knex, table: string) => () => knex.raw(` +export const onUpdateTrigger = (knex: Knex, table: string) => () => + knex.raw(` CREATE TRIGGER ${table}_updated_at BEFORE UPDATE ON ${table} FOR EACH ROW EXECUTE PROCEDURE on_update_timestamp(); `); - diff --git a/api/src/backend/index.ts b/api/src/backend/index.ts index 37f764f4..f05726c5 100644 --- a/api/src/backend/index.ts +++ b/api/src/backend/index.ts @@ -17,9 +17,9 @@ import path from "path"; //import { createBrowserHistory } from "history"; declare global { - interface ErrorConstructor { - new (message?: string, opts?: { cause: Error }): Error; - } + interface ErrorConstructor { + new (message?: string, opts?: { cause: Error }): Error; + } } const port = process.env.PORT || config.port; @@ -40,16 +40,16 @@ app.use(authenticationMiddleware); app.use("/api/v1", v1routes); app.get("/redirect", (req, res) => { - const next = (req.session as any).next; - if (next) { - delete (req.session as any).next; - return res.redirect(next); - } - res.redirect("/"); + const next = (req.session as any).next; + if (next) { + delete (req.session as any).next; + return res.redirect(next); + } + res.redirect("/"); }); app.get("/", (req, res) => { - res.redirect("/dapp"); + res.redirect("/dapp"); }); // uncaught error @@ -67,7 +67,7 @@ app.use(errorHandler(environment)); const server = http.createServer(app); server.on("listening", () => { - const addr = server.address() as AddressInfo; - console.log(`Service started on port ${addr.port}`); + const addr = server.address() as AddressInfo; + console.log(`Service started on port ${addr.port}`); }); server.listen(port); diff --git a/api/src/backend/middleware/authentication/index.ts b/api/src/backend/middleware/authentication/index.ts index 71eb3e01..95f83519 100644 --- a/api/src/backend/middleware/authentication/index.ts +++ b/api/src/backend/middleware/authentication/index.ts @@ -1,6 +1,9 @@ import express from "express"; import passport from "passport"; -import { polkadotJsAuthRouter, polkadotJsStrategy } from "./strategies/web3/polkadot-js"; +import { + polkadotJsAuthRouter, + polkadotJsStrategy, +} from "./strategies/web3/polkadot-js"; import { imbueJsAuthRouter, imbueStrategy } from "./strategies/imbue"; passport.use(polkadotJsStrategy); passport.use(imbueStrategy); @@ -11,13 +14,9 @@ router.use("/auth/web3/polkadot-js", polkadotJsAuthRouter); router.use("/auth/imbue", imbueJsAuthRouter); router.get("/logout", (req, res, next) => { - const redirect: string = req.query.n as string; - res.clearCookie("access_token"); - res.redirect( - redirect - ? redirect - : "/dapp" - ); + const redirect: string = req.query.n as string; + res.clearCookie("access_token"); + res.redirect(redirect ? redirect : "/dapp"); }); export default router; diff --git a/api/src/backend/middleware/authentication/strategies/common.ts b/api/src/backend/middleware/authentication/strategies/common.ts index 70f40c40..cd2e0d53 100644 --- a/api/src/backend/middleware/authentication/strategies/common.ts +++ b/api/src/backend/middleware/authentication/strategies/common.ts @@ -1,68 +1,81 @@ // @ts-ignore -import * as passportJwt from "passport-jwt" +import * as passportJwt from "passport-jwt"; // @ts-ignore -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; export const ensureParams = ( - record: Record, - next: CallableFunction, - params: string[] + record: Record, + next: CallableFunction, + params: string[] ) => { - try { - for (let name of params) { - if (!(record[name] && String(record[name]).trim())) { - throw new Error(`Missing ${name} param.`); - } - } - } catch (e) { - next(e); - } -} + try { + for (let name of params) { + if (!(record[name] && String(record[name]).trim())) { + throw new Error(`Missing ${name} param.`); + } + } + } catch (e) { + next(e); + } +}; -export const cookieExtractor = function(req: any) { - let token: any | null = null; - if (req && req.cookies) token = req.cookies['access_token']; - return token; +export const cookieExtractor = function (req: any) { + let token: any | null = null; + if (req && req.cookies) token = req.cookies["access_token"]; + return token; }; -export function verifyUserIdFromJwt(req: any, res: any, next: any, user_id: number) { - const token = req.cookies.access_token; - if (!token) { - return res.status(401).send("You are not authorized to access this resource."); - } - - try { - const decoded: any = jwt.verify(token, jwtOptions.secretOrKey); - if (user_id == decoded.id) { - return res; - } else { - return res.status(401).send("You are not authorized to access this resource."); - } - } catch (error) { - return res.status(401).send("Invalid token."); - } +export function verifyUserIdFromJwt( + req: any, + res: any, + next: any, + user_id: number +) { + const token = req.cookies.access_token; + if (!token) { + return res + .status(401) + .send("You are not authorized to access this resource."); } -export function validateUserFromJwt(req: any, res: any, next: any, user_id: number) { - const token = req.cookies.access_token; - if (!token) { - return false; + try { + const decoded: any = jwt.verify(token, jwtOptions.secretOrKey); + if (user_id == decoded.id) { + return res; + } else { + return res + .status(401) + .send("You are not authorized to access this resource."); } + } catch (error) { + return res.status(401).send("Invalid token."); + } +} + +export function validateUserFromJwt( + req: any, + res: any, + next: any, + user_id: number +) { + const token = req.cookies.access_token; + if (!token) { + return false; + } - try { - const decoded = jwt.verify(token, jwtOptions.secretOrKey) as jwt.JwtPayload; - if (user_id == decoded.id) { - return true - } else { - return false; - } - } catch (error) { - return false; + try { + const decoded = jwt.verify(token, jwtOptions.secretOrKey) as jwt.JwtPayload; + if (user_id == decoded.id) { + return true; + } else { + return false; } + } catch (error) { + return false; + } } - export const jwtOptions = { - jwtFromRequest: cookieExtractor, - secretOrKey: process.env.JWTSecret ?? 'mysecretword' -}; \ No newline at end of file + jwtFromRequest: cookieExtractor, + secretOrKey: process.env.JWTSecret ?? "mysecretword", +}; diff --git a/api/src/backend/middleware/authentication/strategies/google-oidc.ts b/api/src/backend/middleware/authentication/strategies/google-oidc.ts index 2e5291ba..ae2843c7 100644 --- a/api/src/backend/middleware/authentication/strategies/google-oidc.ts +++ b/api/src/backend/middleware/authentication/strategies/google-oidc.ts @@ -12,60 +12,71 @@ const OIDCStrategy = require("passport-openidconnect"); * prevent the server from starting, so instead of allowing that to happen, we * just spoof these values which effectively disables google OIDC locally. */ -const hasGoogleCreds = Object.entries(config.oidc.google).every(([_,v]) => v); +const hasGoogleCreds = Object.entries(config.oidc.google).every(([_, v]) => v); const googleClientCredentials = hasGoogleCreds - ? config.oidc.google - : { - clientID: "garbage", - clientSecret: "garbage", + ? config.oidc.google + : { + clientID: "garbage", + clientSecret: "garbage", }; - export class GoogleOIDCStrategy extends OIDCStrategy { - name: string; + name: string; - constructor(options: Record, verify: CallableFunction) { - super({ - ...config.oidc.google, - // i.e., this will override the `clientID` and `clientSecret` from - // `config.oicd.google`, but preserve the other entries it had (see - // comment above for declaration of `googleClientCredentials`): - ...googleClientCredentials, - ...options - }, verify); - this.name = "google"; - } + constructor(options: Record, verify: CallableFunction) { + super( + { + ...config.oidc.google, + // i.e., this will override the `clientID` and `clientSecret` from + // `config.oicd.google`, but preserve the other entries it had (see + // comment above for declaration of `googleClientCredentials`): + ...googleClientCredentials, + ...options, + }, + verify + ); + this.name = "google"; + } - authenticate(req: Express.Request, opts?: Record) { - super.authenticate(req, opts); - } + authenticate(req: Express.Request, opts?: Record) { + super.authenticate(req, opts); + } } export const googleOIDCStrategy = new GoogleOIDCStrategy( - { - // callbackURL: "/oauth2/redirect/accounts.google.com", - scope: ["profile"], - // state: true, - }, - (issuer: string, profile: Record, done: CallableFunction) => { - return getOrCreateFederatedUser(issuer, profile.id, profile.displayName, done); - } + { + // callbackURL: "/oauth2/redirect/accounts.google.com", + scope: ["profile"], + // state: true, + }, + (issuer: string, profile: Record, done: CallableFunction) => { + return getOrCreateFederatedUser( + issuer, + profile.id, + profile.displayName, + done + ); + } ); export const googleOIDCRouter = express.Router(); -googleOIDCRouter.get("/login", (req, _res, next) => { +googleOIDCRouter.get( + "/login", + (req, _res, next) => { if (req.query.n) { - (req.session as any).next = req.query.n; + (req.session as any).next = req.query.n; } next(); -}, passport.authenticate("google")); + }, + passport.authenticate("google") +); -googleOIDCRouter.get("/redirect", passport.authenticate( - "google", - { - successReturnToOrRedirect: "/redirect", - // We don't have any error page yet - failureRedirect: "/" - } -)); +googleOIDCRouter.get( + "/redirect", + passport.authenticate("google", { + successReturnToOrRedirect: "/redirect", + // We don't have any error page yet + failureRedirect: "/", + }) +); diff --git a/api/src/backend/middleware/authentication/strategies/imbue.ts b/api/src/backend/middleware/authentication/strategies/imbue.ts index f20de2a5..d9f020d7 100644 --- a/api/src/backend/middleware/authentication/strategies/imbue.ts +++ b/api/src/backend/middleware/authentication/strategies/imbue.ts @@ -1,131 +1,145 @@ import express from "express"; import type { Session } from "express-session"; import passport, { use } from "passport"; -import { generateGetStreamToken, getOrCreateFederatedUser, updateFederatedLoginUser, updateUserGetStreamToken, User } from "../../../models"; +import { + generateGetStreamToken, + getOrCreateFederatedUser, + updateFederatedLoginUser, + updateUserGetStreamToken, + User, +} from "../../../models"; import config from "../../../config"; import db from "../../../db"; import * as models from "../../../models"; -import { ensureParams, cookieExtractor, jwtOptions } from "./common" +import { ensureParams, cookieExtractor, jwtOptions } from "./common"; // @ts-ignore -import * as passportJwt from "passport-jwt" +import * as passportJwt from "passport-jwt"; // @ts-ignore -import jwt from 'jsonwebtoken'; -import bcrypt from 'bcryptjs' -import { StreamChat } from 'stream-chat'; - +import jwt from "jsonwebtoken"; +import bcrypt from "bcryptjs"; +import { StreamChat } from "stream-chat"; const JwtStrategy = passportJwt.Strategy; export const imbueJsAuthRouter = express.Router(); // @ts-ignore -export const imbueStrategy = new JwtStrategy(jwtOptions, async function (jwt_payload, next) { - const id = jwt_payload.id; - try { - const user = await db.select().from("users").where({ "id": Number(id) }).first(); - if (!user) { - next(`No user found with id: ${id}`, false); - } else { - return next(null, { id: user.id, username: user.username, getstream_token: user.getstream_token, display_name: user.display_name }); - } - } catch (e) { - return next(`Failed to deserialize user with id ${id}`, false); +export const imbueStrategy = new JwtStrategy(jwtOptions, async function ( + jwt_payload, + next +) { + const id = jwt_payload.id; + try { + const user = await db + .select() + .from("users") + .where({ id: Number(id) }) + .first(); + if (!user) { + next(`No user found with id: ${id}`, false); + } else { + return next(null, { + id: user.id, + username: user.username, + getstream_token: user.getstream_token, + display_name: user.display_name, + }); } + } catch (e) { + return next(`Failed to deserialize user with id ${id}`, false); + } }); imbueJsAuthRouter.post("/", (req, res, next) => { - const { - userOrEmail, - password - } = req.body; + const { userOrEmail, password } = req.body; + + db.transaction(async (tx) => { + try { + const user = await models.fetchUserOrEmail(userOrEmail)(tx); + if (!user) { + return res.status(404).end(); + } + + if (!user.getstream_token) { + const token = await generateGetStreamToken(user); + await updateUserGetStreamToken(user?.id, token)(tx); + } + + const loginSuccessful = await bcrypt.compare(password, user.password); + if (!loginSuccessful) { + return res.status(404).end(); + } + + const payload = { id: user.id }; + const token = jwt.sign(payload, jwtOptions.secretOrKey); + res.cookie("access_token", token, { + secure: config.environment !== "development", + httpOnly: true, + }); + + res.send({ id: user.id, display_name: user.display_name }); + } catch (e) { + next( + new Error(`Failed to fetch user ${userOrEmail}`, { + cause: e as Error, + }) + ); + } + }); +}); - db.transaction(async tx => { - try { - const user = await models.fetchUserOrEmail(userOrEmail)(tx); - if (!user) { - return res.status(404).end(); - } +imbueJsAuthRouter.post("/register", (req, res, next) => { + const { username, email, password } = req.body; + + ensureParams(req.body, next, ["username", "email", "password"]); + + db.transaction(async (tx) => { + const usernameExists = await models.fetchUserOrEmail(username)(tx); + const emailExists = await models.fetchUserOrEmail(email)(tx); + if (usernameExists) { + return res.status(409).send(JSON.stringify("Username already exists.")); + } else if (emailExists) { + return res.status(409).send(JSON.stringify("Email already exists.")); + } else { + let updateUserDetails = async (err: Error, user: User) => { + if (err) { + next(err); + } - if(!user.getstream_token) { - const token = await generateGetStreamToken(user); - await updateUserGetStreamToken(user?.id, token)(tx); - } + if (!user) { + next(new Error("No user provided.")); + } - const loginSuccessful = await bcrypt.compare(password, user.password) - if (!loginSuccessful) { - return res.status(404).end(); - } + db.transaction(async (tx) => { + try { + await updateFederatedLoginUser(user, username, email, password)(tx); const payload = { id: user.id }; const token = jwt.sign(payload, jwtOptions.secretOrKey); res.cookie("access_token", token, { - secure: config.environment !== "development", - httpOnly: true + secure: config.environment !== "development", + httpOnly: true, }); - - res.send({ id: user.id, display_name: user.display_name }); - } catch (e) { - next(new Error( - `Failed to fetch user ${userOrEmail}`, - { cause: e as Error } - )); - } - }); + res.send({ + id: user.id, + display_name: user.display_name, + }); + } catch (e) { + tx.rollback(); + next( + new Error(`Unable to upsert details for user: ${username}`, { + cause: e as Error, + }) + ); + } + }); + }; + + getOrCreateFederatedUser( + "Imbue Network", + username.toLowerCase(), + username.toLowerCase(), + updateUserDetails + ); + } + }); }); - -imbueJsAuthRouter.post("/register", (req, res, next) => { - const { - username, - email, - password - } = req.body; - - ensureParams(req.body, next, ["username", "email", "password"]); - - db.transaction(async tx => { - const usernameExists = await models.fetchUserOrEmail(username)(tx); - const emailExists = await models.fetchUserOrEmail(email)(tx); - if (usernameExists) { - return res.status(409).send(JSON.stringify('Username already exists.')); - } else if (emailExists) { - return res.status(409).send(JSON.stringify('Email already exists.')); - } else { - let updateUserDetails = async (err: Error, user: User) => { - if (err) { - next(err); - } - - if (!user) { - next(new Error("No user provided.")); - } - - db.transaction(async tx => { - try { - await updateFederatedLoginUser( - user, username, email, password - )(tx); - - const payload = { id: user.id }; - const token = jwt.sign(payload, jwtOptions.secretOrKey); - res.cookie("access_token", token, { - secure: config.environment !== "development", - httpOnly: true - }); - res.send({ id: user.id, display_name: user.display_name }); - } catch (e) { - tx.rollback(); - next(new Error( - `Unable to upsert details for user: ${username}`, - { cause: e as Error } - )); - } - }); - }; - - getOrCreateFederatedUser( - "Imbue Network", - username.toLowerCase(), - username.toLowerCase(), - updateUserDetails); - } - }); -}); \ No newline at end of file diff --git a/api/src/backend/middleware/authentication/strategies/web3/polkadot-js.ts b/api/src/backend/middleware/authentication/strategies/web3/polkadot-js.ts index 0df38adc..29a4bb1c 100644 --- a/api/src/backend/middleware/authentication/strategies/web3/polkadot-js.ts +++ b/api/src/backend/middleware/authentication/strategies/web3/polkadot-js.ts @@ -1,181 +1,181 @@ import express from "express"; -import {v4 as uuid} from "uuid"; +import { v4 as uuid } from "uuid"; -import {signatureVerify} from "@polkadot/util-crypto"; +import { signatureVerify } from "@polkadot/util-crypto"; -import {decodeAddress, encodeAddress} from "@polkadot/keyring"; -import {hexToU8a, isHex} from '@polkadot/util'; -import {ensureParams, cookieExtractor, jwtOptions} from "../common" +import { decodeAddress, encodeAddress } from "@polkadot/keyring"; +import { hexToU8a, isHex } from "@polkadot/util"; +import { ensureParams, cookieExtractor, jwtOptions } from "../common"; import { - fetchUser, - fetchWeb3Account, - getOrCreateFederatedUser, - upsertWeb3Challenge, - User, - Web3Account + fetchUser, + fetchWeb3Account, + getOrCreateFederatedUser, + upsertWeb3Challenge, + User, + Web3Account, } from "../../../../models"; import db from "../../../../db"; - // @ts-ignore -import * as passportJwt from "passport-jwt" +import * as passportJwt from "passport-jwt"; // @ts-ignore -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; import config from "../../../../config"; -import { StreamChat } from 'stream-chat'; +import { StreamChat } from "stream-chat"; const JwtStrategy = passportJwt.Strategy; type Solution = { - signature: string; - address: string; - type: string; + signature: string; + address: string; + type: string; }; - - // @ts-ignore -export const polkadotJsStrategy = new JwtStrategy(jwtOptions, async function(jwt_payload, next) { - const id = jwt_payload.id; - - try { - const user = await db.select().from("users").where({"id": Number(id)}).first(); - if (!user) { - next(`No user found with id: ${id}`, false); - } else { - user.web3Accounts = await db("web3_accounts").select().where({ - user_id: user.id - }); +export const polkadotJsStrategy = new JwtStrategy(jwtOptions, async function ( + jwt_payload, + next +) { + const id = jwt_payload.id; + + try { + const user = await db + .select() + .from("users") + .where({ id: Number(id) }) + .first(); + if (!user) { + next(`No user found with id: ${id}`, false); + } else { + user.web3Accounts = await db("web3_accounts") + .select() + .where({ + user_id: user.id, + }); - return next(null, user); - } - } catch (e) { - return next(`Failed to deserialize user with id ${id}`, false); + return next(null, user); } + } catch (e) { + return next(`Failed to deserialize user with id ${id}`, false); + } }); - export const polkadotJsAuthRouter = express.Router(); polkadotJsAuthRouter.post("/", (req, res, next) => { - ensureParams(req.body, next, ["address", "meta", "type"]); - ensureParams(req.body.meta, next, ["name", "source"]); - - const address = req.body.address.trim(); - - try { - encodeAddress( - isHex(address) - ? hexToU8a(address) - : decodeAddress(address) - ); - } catch (e) { - const err = new Error( - "Invalid `address` param.", - {cause: e as Error} - ); - (err as any).status = 400; + ensureParams(req.body, next, ["address", "meta", "type"]); + ensureParams(req.body.meta, next, ["name", "source"]); + + const address = req.body.address.trim(); + + try { + encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); + } catch (e) { + const err = new Error("Invalid `address` param.", { + cause: e as Error, + }); + (err as any).status = 400; + next(err); + } + + // If no address can be found, create a `users` and then a + // `federated_credential` + getOrCreateFederatedUser( + req.body.meta.source, + address, + req.body.meta.name, + async (err: Error, user: User) => { + if (err) { next(err); + } + + if (!user) { + next(new Error("No user provided.")); + } + + // create a `challenge` uuid and insert it into the users + // table respond with the challenge + db.transaction(async (tx) => { + try { + const challenge = uuid(); + const [web3Account, isInsert] = await upsertWeb3Challenge( + user, + address, + req.body.type, + challenge + )(tx); + + if (isInsert) { + res.status(201); + } + + res.send({ user, web3Account }); + } catch (e) { + await tx.rollback(); + next( + new Error( + `Unable to upsert web3 challenge for address: ${address}`, + { cause: e as Error } + ) + ); + } + }); } + ); +}); - - - - // If no address can be found, create a `users` and then a - // `federated_credential` - getOrCreateFederatedUser( - req.body.meta.source, - address, - req.body.meta.name, - async (err: Error, user: User) => { - if (err) { - next(err); - } - - if (!user) { - next(new Error("No user provided.")); - } - - // create a `challenge` uuid and insert it into the users - // table respond with the challenge - db.transaction(async tx => { - try { - const challenge = uuid(); - const [web3Account, isInsert] = await upsertWeb3Challenge( - user, address, req.body.type, challenge - )(tx); - - if (isInsert) { - res.status(201); - } - - res.send({user, web3Account}); - } catch (e) { - await tx.rollback(); - next(new Error( - `Unable to upsert web3 challenge for address: ${address}`, - {cause: e as Error} - )); - } +polkadotJsAuthRouter.post("/callback", (req, res, next) => { + db.transaction(async (tx) => { + try { + const solution: Solution = req.body; + const web3Account = await fetchWeb3Account(solution.address)(tx); + + if (!web3Account) { + res.status(404); + } else { + const user = await fetchUser(web3Account.user_id)(tx); + if (user?.id) { + if ( + signatureVerify( + web3Account.challenge, + solution.signature, + solution.address + ).isValid + ) { + const payload = { id: user?.id }; + const token = jwt.sign(payload, jwtOptions.secretOrKey); + + res.cookie("access_token", token, { + secure: config.environment !== "development", + httpOnly: true, }); - } - ); -}); -polkadotJsAuthRouter.post( - "/callback", - (req, res, next) => { - db.transaction(async tx => { - try { - const solution: Solution = req.body; - const web3Account = await fetchWeb3Account( - solution.address - )(tx); - - if (!web3Account) { - res.status(404); - } else { - const user = await fetchUser(web3Account.user_id)(tx); - if (user?.id) { - if (signatureVerify(web3Account.challenge, solution.signature, solution.address).isValid) { - const payload = {id: user?.id}; - const token = jwt.sign(payload, jwtOptions.secretOrKey); - - res.cookie("access_token", token, { - secure: config.environment !== "development", - httpOnly: true - }); - - res.send({success: true}); - } else { - const challenge = uuid(); - const [web3Account, _] = await upsertWeb3Challenge( - user, - solution.address, - solution.type, - challenge - )(tx); - - /** - * FIXME: this sets the "WWW-Authenticate" header. - * Should we be running all of the auth calls - * through the same endpoint and responding with - * the "challenge" here, instead? Also, what form - * should this actually take? - */ - next(`Imbue ${web3Account.challenge}`); - } - } else { - res.status(404); - } - } - } catch (e) { - await tx.rollback(); - next(new Error( - `Unable to finalise login`, - {cause: e as Error} - )); - } - }); + res.send({ success: true }); + } else { + const challenge = uuid(); + const [web3Account, _] = await upsertWeb3Challenge( + user, + solution.address, + solution.type, + challenge + )(tx); + + /** + * FIXME: this sets the "WWW-Authenticate" header. + * Should we be running all of the auth calls + * through the same endpoint and responding with + * the "challenge" here, instead? Also, what form + * should this actually take? + */ + next(`Imbue ${web3Account.challenge}`); + } + } else { + res.status(404); + } + } + } catch (e) { + await tx.rollback(); + next(new Error(`Unable to finalise login`, { cause: e as Error })); } -); + }); +}); diff --git a/api/src/backend/middleware/errors.ts b/api/src/backend/middleware/errors.ts index 95592c0e..2f7db875 100644 --- a/api/src/backend/middleware/errors.ts +++ b/api/src/backend/middleware/errors.ts @@ -1,36 +1,33 @@ import express, { ErrorRequestHandler } from "express"; - interface MiddlewareError extends Error { - cause: MiddlewareError; - status: number; + cause: MiddlewareError; + status: number; } -export const errorHandler = (environment: string): ErrorRequestHandler => ( +export const errorHandler = + (environment: string): ErrorRequestHandler => + ( err: MiddlewareError, _req: express.Request, res: express.Response, _next: express.NextFunction -) => { + ) => { const causes = []; let cause = err; console.error(cause); - - while (cause = cause.cause) { - causes.push(cause); - console.error("Caused by:", cause); - }; + + while ((cause = cause.cause)) { + causes.push(cause); + console.error("Caused by:", cause); + } res.status(err.status || 500); res.send({ - message: err.message, - ...( - environment.startsWith("dev") - ? {causes: causes.map( - cause => cause.message - )} - : {} - ) + message: err.message, + ...(environment.startsWith("dev") + ? { causes: causes.map((cause) => cause.message) } + : {}), }); -}; \ No newline at end of file + }; diff --git a/api/src/backend/models.ts b/api/src/backend/models.ts index 73e66ea4..3b2a3cb5 100644 --- a/api/src/backend/models.ts +++ b/api/src/backend/models.ts @@ -1,790 +1,852 @@ import { knex, Knex } from "knex"; import db from "./db/index"; -import { StreamChat } from 'stream-chat'; +import { StreamChat } from "stream-chat"; export type FederatedCredential = { - id: number, - issuer: string, - subject: string, + id: number; + issuer: string; + subject: string; }; export type Skill = { - id: number, - name: string -} + id: number; + name: string; +}; export type Industry = { - id: number, - name: string -} + id: number; + name: string; +}; export type Language = { - id: number, - name: string -} + id: number; + name: string; +}; export type Service = { - id: number, - name: string -} + id: number; + name: string; +}; export type Web3Account = { - address: string, - user_id: number; - type: string; - challenge: string; + address: string; + user_id: number; + type: string; + challenge: string; }; export type User = { - id: number; - display_name: string; - web3Accounts: Web3Account[]; - username: string; - email: string; - password: string; - briefs_submitted: number; - getstream_token: string; + id: number; + display_name: string; + web3Accounts: Web3Account[]; + username: string; + email: string; + password: string; + briefs_submitted: number; + getstream_token: string; }; export type ProposedMilestone = { - name: string; - percentage_to_unlock: number; - amount: number; + name: string; + percentage_to_unlock: number; + amount: number; }; export type GrantProposal = { - name: string; - logo: string; - description: string; - website: string; - milestones: ProposedMilestone[]; - required_funds: number; - owner?: string; - user_id?: number; - category?: string | number; - currency_id: number; - chain_project_id?: number; + name: string; + logo: string; + description: string; + website: string; + milestones: ProposedMilestone[]; + required_funds: number; + owner?: string; + user_id?: number; + category?: string | number; + currency_id: number; + chain_project_id?: number; }; export type Milestone = ProposedMilestone & { - milestone_index: number; - project_id: number | string; - is_approved: boolean; + milestone_index: number; + project_id: number | string; + is_approved: boolean; }; export type MilestoneDetails = { - index: number | string; - project_id: number | string; - details: string; -} + index: number | string; + project_id: number | string; + details: string; +}; export type Project = { - id?: string | number; - name: string; - logo: string; - description: string; - website: string; - category?: string | number; - chain_project_id?: number; - required_funds: number; - currency_id: number; - owner?: string; - user_id?: string | number; - brief_id?: string | number; - total_cost_without_fee?: number; - imbue_fee?: number; + id?: string | number; + name: string; + logo: string; + description: string; + website: string; + category?: string | number; + chain_project_id?: number; + required_funds: number; + currency_id: number; + owner?: string; + user_id?: string | number; + brief_id?: string | number; + total_cost_without_fee?: number; + imbue_fee?: number; }; export type ProjectProperties = { - id?: string | number; - faq: string; - project_id?: string | number; + id?: string | number; + faq: string; + project_id?: string | number; }; // created_by: string; // hours_per_week: number, // briefs_submitted_by: number, export type Brief = { - id?: string | number; - headline: string; - industry_ids: number[]; - industries: string[]; - description: string; - skill_ids: number[]; - skills: string[]; - scope_id: number; - scope_level: string; - duration_id: number; - duration: string; - budget: bigint; - experience_level: string, - experience_id: number - user_id: number; + id?: string | number; + headline: string; + industry_ids: number[]; + industries: string[]; + description: string; + skill_ids: number[]; + skills: string[]; + scope_id: number; + scope_level: string; + duration_id: number; + duration: string; + budget: bigint; + experience_level: string; + experience_id: number; + user_id: number; }; export type Freelancer = { - id: string | number; - bio: string; - education: string; - experience: string; - facebook_link: string; - twitter_link: string; - telegram_link: string; - discord_link: string; - freelanced_before: string; - freelancing_goal: string; - work_type: string; - title: string; - skills: string[]; - languages: string[]; - services: string[]; - clients: string[]; - client_images: string[]; - display_name: string; - username: string; - user_id: number; - rating?: number; - num_ratings: number; + id: string | number; + bio: string; + education: string; + experience: string; + facebook_link: string; + twitter_link: string; + telegram_link: string; + discord_link: string; + freelanced_before: string; + freelancing_goal: string; + work_type: string; + title: string; + skills: string[]; + languages: string[]; + services: string[]; + clients: string[]; + client_images: string[]; + display_name: string; + username: string; + user_id: number; + rating?: number; + num_ratings: number; }; - export type BriefSqlFilter = { - experience_range: number[]; - submitted_range: number[]; - submitted_is_max: boolean; - length_range: number[]; - length_is_max: boolean; - search_input: string; + experience_range: number[]; + submitted_range: number[]; + submitted_is_max: boolean; + length_range: number[]; + length_is_max: boolean; + search_input: string; }; export type FreelancerSqlFilter = { - skills_range: Array; - services_range: Array; - languages_range: Array; - search_input: string; + skills_range: Array; + services_range: Array; + languages_range: Array; + search_input: string; }; -export const fetchWeb3Account = (address: string) => - (tx: Knex.Transaction) => - tx("web3_accounts") - .select() - .where({ address, }) - .first(); - -export const fetchUser = (id: number) => - (tx: Knex.Transaction) => - tx("users").where({ id }).first(); - -export const fetchUserOrEmail = (userOrEmail: string) => - (tx: Knex.Transaction) => - tx("users").where({ username: userOrEmail }) - .orWhere({ email: userOrEmail.toLowerCase() }) - .first() - .debug(true); - -export const upsertWeb3Challenge = ( - user: User, - address: string, - type: string, - challenge: string, -) => async (tx: Knex.Transaction): - Promise<[web3Account: Web3Account, isInsert: boolean]> => { - - const web3Account = await tx("web3_accounts") - .select() - .where({ - user_id: user?.id +export const fetchWeb3Account = (address: string) => (tx: Knex.Transaction) => + tx("web3_accounts").select().where({ address }).first(); + +export const fetchUser = (id: number) => (tx: Knex.Transaction) => + tx("users").where({ id }).first(); + +export const fetchUserOrEmail = + (userOrEmail: string) => (tx: Knex.Transaction) => + tx("users") + .where({ username: userOrEmail }) + .orWhere({ email: userOrEmail.toLowerCase() }) + .first() + .debug(true); + +export const upsertWeb3Challenge = + (user: User, address: string, type: string, challenge: string) => + async ( + tx: Knex.Transaction + ): Promise<[web3Account: Web3Account, isInsert: boolean]> => { + const web3Account = await tx("web3_accounts") + .select() + .where({ + user_id: user?.id, + }) + .first(); + + if (!web3Account) { + return [ + ( + await tx("web3_accounts") + .insert({ + address, + user_id: user.id, + type, + challenge, }) - .first(); - - if (!web3Account) { - return [ - ( - await tx("web3_accounts").insert({ - address, - user_id: user.id, - type, - challenge, - }).returning("*") - )[0], - true - ]; - } + .returning("*") + )[0], + true, + ]; + } - return [ - ( - await tx("web3_accounts").update({ challenge }).where( - { user_id: user.id } - ).returning("*") - )[0], - false - ]; - }; - -export const insertUserByDisplayName = (displayName: string, username: string) => - async (tx: Knex.Transaction) => ( - await tx("users").insert({ - display_name: displayName, - username: username, - }).returning("*") + return [ + ( + await tx("web3_accounts") + .update({ challenge }) + .where({ user_id: user.id }) + .returning("*") + )[0], + false, + ]; + }; + +export const insertUserByDisplayName = + (displayName: string, username: string) => async (tx: Knex.Transaction) => + ( + await tx("users") + .insert({ + display_name: displayName, + username: username, + }) + .returning("*") )[0]; export const generateGetStreamToken = async (user: User) => { - if (process.env.REACT_APP_GETSTREAM_API_KEY && process.env.REACT_APP_GETSTREAM_SECRET_KEY) { - const client: StreamChat = new StreamChat(process.env.REACT_APP_GETSTREAM_API_KEY, process.env.REACT_APP_GETSTREAM_SECRET_KEY); - const token = client.createToken(user.username); - await client.upsertUser({ id: user.username}); - return token; - } - return "" -} - -export const updateUserGetStreamToken = (id: number, token: string ) => - async (tx: Knex.Transaction) => ( - await tx("users").where({id}).update({ - getstream_token: token - }).returning("*") - )[0]; + if ( + process.env.REACT_APP_GETSTREAM_API_KEY && + process.env.REACT_APP_GETSTREAM_SECRET_KEY + ) { + const client: StreamChat = new StreamChat( + process.env.REACT_APP_GETSTREAM_API_KEY, + process.env.REACT_APP_GETSTREAM_SECRET_KEY + ); + const token = client.createToken(user.username); + await client.upsertUser({ id: user.username }); + return token; + } + return ""; +}; -export const insertToTable = (item: string, table_name: string) => - async (tx: Knex.Transaction) => ( - await tx(table_name).insert({ - name: item.toLowerCase() - }).returning("*") +export const updateUserGetStreamToken = + (id: number, token: string) => async (tx: Knex.Transaction) => + ( + await tx("users") + .where({ id }) + .update({ + getstream_token: token, + }) + .returning("*") )[0]; -export const updateFederatedLoginUser = (user: User, username: string, email: string, password: string) => - async (tx: Knex.Transaction) => ( - await tx("users").update({ - username: username.toLowerCase(), - email: email.toLowerCase(), - password: password - }).where({ - id: user.id - }).returning("*") +export const insertToTable = + (item: string, table_name: string) => + async (tx: Knex.Transaction) => + ( + await tx(table_name) + .insert({ + name: item.toLowerCase(), + }) + .returning("*") )[0]; -export const insertProject = (project: Project) => - async (tx: Knex.Transaction) => ( - await tx("projects").insert(project).returning("*") +export const updateFederatedLoginUser = + (user: User, username: string, email: string, password: string) => + async (tx: Knex.Transaction) => + ( + await tx("users") + .update({ + username: username.toLowerCase(), + email: email.toLowerCase(), + password: password, + }) + .where({ + id: user.id, + }) + .returning("*") )[0]; -export const updateProject = (id: string | number, project: Project) => - async (tx: Knex.Transaction) => ( - await tx("projects") - .update(project) - .where({ id }) - .returning("*") - )[0]; +export const insertProject = + (project: Project) => async (tx: Knex.Transaction) => + (await tx("projects").insert(project).returning("*"))[0]; -export const updateProjectProperties = (id: string | number, properties: ProjectProperties) => - async (tx: Knex.Transaction) => ( - await tx("project_properties") - .update(properties) - .where({ 'project_id': id }) - .returning("*") +export const updateProject = + (id: string | number, project: Project) => async (tx: Knex.Transaction) => + ( + await tx("projects").update(project).where({ id }).returning("*") )[0]; +export const updateProjectProperties = + (id: string | number, properties: ProjectProperties) => + async (tx: Knex.Transaction) => + ( + await tx("project_properties") + .update(properties) + .where({ project_id: id }) + .returning("*") + )[0]; -export const fetchUserBriefApplications = (user_id: string | number, brief_id: string | number) => -(tx: Knex.Transaction) => +export const fetchUserBriefApplications = + (user_id: string | number, brief_id: string | number) => + (tx: Knex.Transaction) => tx("projects").select().where({ user_id, brief_id }).first(); -export const fetchProject = (id: string | number) => - (tx: Knex.Transaction) => - tx("projects").select().where({ id }).first(); - - -export const fetchProjectWithProperties = (id: string | number) => - (tx: Knex.Transaction) => - tx("projects").join("project_properties", "projects.id", "=", "project_properties.project_id").select().where({ "project_id": id }).first(); - -export const fetchAllProjects = () => - (tx: Knex.Transaction) => - tx("projects").select(); - -export const fetchUserProject = (id: string | number) => - (tx: Knex.Transaction) => - tx("projects").select().where({ - user_id: id - }).first(); +export const fetchProject = (id: string | number) => (tx: Knex.Transaction) => + tx("projects").select().where({ id }).first(); + +export const fetchProjectWithProperties = + (id: string | number) => (tx: Knex.Transaction) => + tx("projects") + .join( + "project_properties", + "projects.id", + "=", + "project_properties.project_id" + ) + .select() + .where({ project_id: id }) + .first(); + +export const fetchAllProjects = () => (tx: Knex.Transaction) => + tx("projects").select(); + +export const fetchUserProject = + (id: string | number) => (tx: Knex.Transaction) => + tx("projects") + .select() + .where({ + user_id: id, + }) + .first(); export const insertMilestones = ( - milestones: ProposedMilestone[], - project_id: string | number, + milestones: ProposedMilestone[], + project_id: string | number ) => { - const values = milestones.map((m, idx) => ({ - ...m, - project_id, - milestone_index: idx, - })); - - return (tx: Knex.Transaction) => - tx("milestones").insert(values).returning("*"); + const values = milestones.map((m, idx) => ({ + ...m, + project_id, + milestone_index: idx, + })); + + return (tx: Knex.Transaction) => + tx("milestones").insert(values).returning("*"); }; -export const deleteMilestones = (project_id: string | number) => - (tx: Knex.Transaction) => - tx("milestones").delete().where({ project_id }); - -export const fetchProjectMilestones = (id: string | number) => - (tx: Knex.Transaction) => - tx("milestones").select().where({ project_id: id }); - -export const updateMilestoneDetails = (id: string | number, milestoneId: string | number, details: string) => (tx: Knex.Transaction) => - tx("milestone_details").where({ project_id: id }).where('index', '=', milestoneId).update('details', details).returning("*"); - -export const insertMilestoneDetails = (value: MilestoneDetails) => async (tx: Knex.Transaction) => (await - tx("milestone_details").insert(value).returning("*"))[0]; - -export const fetchAllMilestone = (id: string | number) => - (tx: Knex.Transaction) => - tx("milestone_details").where('project_id', '=', id); - -export const fetchMilestoneByIndex = (projectId: string | number, milestoneId: string | number) => - (tx: Knex.Transaction) => - tx("milestone_details").select().where({ project_id: projectId }).where('index', '=', milestoneId); - -export const fetchBriefApplications = (id: string) => - (tx: Knex.Transaction) => - fetchAllProjects()(tx) - .where({ "brief_id": id }) - .select() - -export const fetchBriefProjects = (brief_id: string) => - (tx: Knex.Transaction) => - fetchAllProjects()(tx) - .where({ brief_id }) - .select() - -export const fetchAcceptedBriefs = (user_id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ user_id }) - .select() - -export const fetchBrief = (id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ "briefs.id": id }) - .first() - - -export const fetchUserBriefs = (user_id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ user_id }) - .select() - -export const fetchAllBriefs = () => - (tx: Knex.Transaction) => - tx.select( - "briefs.id", - "headline", - "description", - "scope.scope_level", - "briefs.scope_id", - "duration.duration", - "briefs.duration_id", - "budget", - "users.display_name as created_by", - "experience_level", - "briefs.experience_id", - "briefs.created", - "briefs.user_id", - "briefs.project_id", - "users.briefs_submitted as number_of_briefs_submitted", - tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), - tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), - tx.raw("ARRAY_AGG(DISTINCT CAST(industries.name as text)) as industries"), - tx.raw("ARRAY_AGG(DISTINCT CAST(industries.id as text)) as industry_ids"), - ) - .from("briefs") - .leftJoin("brief_industries", { "briefs.id": "brief_industries.brief_id" }) - .leftJoin("industries", { "brief_industries.industry_id": "industries.id" }) - .leftJoin("brief_skills", { "briefs.id": "brief_skills.brief_id" }) - .leftJoin("skills", { "brief_skills.skill_id": "skills.id" }) - .leftJoin("experience", { 'briefs.experience_id': "experience.id" }) - .leftJoin("scope", { "briefs.scope_id": "scope.id" }) - .leftJoin("duration", { "briefs.duration_id": "duration.id" }) - .innerJoin("users", { "briefs.user_id": "users.id" }) - .orderBy("briefs.created", "desc") - .groupBy("briefs.id") - .groupBy("scope.scope_level") - .groupBy("duration.duration") - .groupBy("users.display_name") - .groupBy("briefs.experience_id") - .groupBy("experience.experience_level") - .groupBy("users.id") - -export const fetchItems = (ids: number[], tableName: string) => - async (tx: Knex.Transaction) => - tx(tableName).select("id", "name") - .whereIn(`id`, ids); +export const deleteMilestones = + (project_id: string | number) => (tx: Knex.Transaction) => + tx("milestones").delete().where({ project_id }); + +export const fetchProjectMilestones = + (id: string | number) => (tx: Knex.Transaction) => + tx("milestones").select().where({ project_id: id }); + +export const updateMilestoneDetails = + (id: string | number, milestoneId: string | number, details: string) => + (tx: Knex.Transaction) => + tx("milestone_details") + .where({ project_id: id }) + .where("index", "=", milestoneId) + .update("details", details) + .returning("*"); + +export const insertMilestoneDetails = + (value: MilestoneDetails) => async (tx: Knex.Transaction) => + ( + await tx("milestone_details") + .insert(value) + .returning("*") + )[0]; + +export const fetchAllMilestone = + (id: string | number) => (tx: Knex.Transaction) => + tx("milestone_details").where("project_id", "=", id); + +export const fetchMilestoneByIndex = + (projectId: string | number, milestoneId: string | number) => + (tx: Knex.Transaction) => + tx("milestone_details") + .select() + .where({ project_id: projectId }) + .where("index", "=", milestoneId); + +export const fetchBriefApplications = (id: string) => (tx: Knex.Transaction) => + fetchAllProjects()(tx).where({ brief_id: id }).select(); + +export const fetchBriefProjects = + (brief_id: string) => (tx: Knex.Transaction) => + fetchAllProjects()(tx).where({ brief_id }).select(); + +export const fetchAcceptedBriefs = + (user_id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ user_id }).select(); + +export const fetchBrief = (id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ "briefs.id": id }).first(); + +export const fetchUserBriefs = (user_id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ user_id }).select(); + +export const fetchAllBriefs = () => (tx: Knex.Transaction) => + tx + .select( + "briefs.id", + "headline", + "description", + "scope.scope_level", + "briefs.scope_id", + "duration.duration", + "briefs.duration_id", + "budget", + "users.display_name as created_by", + "experience_level", + "briefs.experience_id", + "briefs.created", + "briefs.user_id", + "briefs.project_id", + "users.briefs_submitted as number_of_briefs_submitted", + tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), + tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), + tx.raw("ARRAY_AGG(DISTINCT CAST(industries.name as text)) as industries"), + tx.raw("ARRAY_AGG(DISTINCT CAST(industries.id as text)) as industry_ids") + ) + .from("briefs") + .leftJoin("brief_industries", { + "briefs.id": "brief_industries.brief_id", + }) + .leftJoin("industries", { + "brief_industries.industry_id": "industries.id", + }) + .leftJoin("brief_skills", { "briefs.id": "brief_skills.brief_id" }) + .leftJoin("skills", { "brief_skills.skill_id": "skills.id" }) + .leftJoin("experience", { "briefs.experience_id": "experience.id" }) + .leftJoin("scope", { "briefs.scope_id": "scope.id" }) + .leftJoin("duration", { "briefs.duration_id": "duration.id" }) + .innerJoin("users", { "briefs.user_id": "users.id" }) + .orderBy("briefs.created", "desc") + .groupBy("briefs.id") + .groupBy("scope.scope_level") + .groupBy("duration.duration") + .groupBy("users.display_name") + .groupBy("briefs.experience_id") + .groupBy("experience.experience_level") + .groupBy("users.id"); + +export const fetchItems = + (ids: number[], tableName: string) => async (tx: Knex.Transaction) => + tx(tableName).select("id", "name").whereIn(`id`, ids); // export const fetchSkills = (ids: string[]) => // (tx: Knex.Transaction) => // tx("skills").select("id","name").whereIn('id', ids ); // Insert a brief and their respective skill and industry_ids. -export const insertBrief = (brief: Brief, skill_ids: number[], industry_ids: number[], scope_id: number, duration_id: number) => - async (tx: Knex.Transaction) => ( - await tx("briefs").insert({ - headline: brief.headline, - description: brief.description, - duration_id: duration_id, - scope_id: scope_id, - user_id: brief.user_id, - budget: brief.budget, - experience_id: brief.experience_id, - }).returning("briefs.id") - .then(async (ids) => { - if (skill_ids) { - skill_ids.forEach(async (skillId) => { - if (skillId) { - await tx("brief_skills") - .insert({ - brief_id: ids[0], - skill_id: skillId - }) - } - - }) - } - - if (industry_ids) { - industry_ids.forEach(async (industry_id) => { - if (industry_id) { - await tx("brief_industries") - .insert({ - brief_id: ids[0], - industry_id: industry_id - }) - } - - }) - } - return ids[0] - }) - ); +export const insertBrief = + ( + brief: Brief, + skill_ids: number[], + industry_ids: number[], + scope_id: number, + duration_id: number + ) => + async (tx: Knex.Transaction) => + await tx("briefs") + .insert({ + headline: brief.headline, + description: brief.description, + duration_id: duration_id, + scope_id: scope_id, + user_id: brief.user_id, + budget: brief.budget, + experience_id: brief.experience_id, + }) + .returning("briefs.id") + .then(async (ids) => { + if (skill_ids) { + skill_ids.forEach(async (skillId) => { + if (skillId) { + await tx("brief_skills").insert({ + brief_id: ids[0], + skill_id: skillId, + }); + } + }); + } -export const incrementUserBriefSubmissions = (id: number) => - async (tx: Knex.Transaction) => ( - tx("users").where({ id: id }).increment('briefs_submitted', 1) - ); + if (industry_ids) { + industry_ids.forEach(async (industry_id) => { + if (industry_id) { + await tx("brief_industries").insert({ + brief_id: ids[0], + industry_id: industry_id, + }); + } + }); + } + return ids[0]; + }); + +export const incrementUserBriefSubmissions = + (id: number) => async (tx: Knex.Transaction) => + tx("users").where({ id: id }).increment("briefs_submitted", 1); + +export const insertFederatedCredential = + (id: number, issuer: string, subject: string) => + async (tx: Knex.Transaction) => + ( + await tx("federated_credentials") + .insert({ + id, + issuer, + subject, + }) + .returning("*") + )[0]; -export const insertFederatedCredential = ( - id: number, - issuer: string, - subject: string, -) => async (tx: Knex.Transaction) => ( - await tx("federated_credentials").insert({ - id, issuer, subject - }).returning("*") -)[0]; - -export const upsertItems = (items: string[], tableName: string) => async (tx: Knex.Transaction) => { +export const upsertItems = + (items: string[], tableName: string) => async (tx: Knex.Transaction) => { var item_ids: number[] = []; try { - //TODO Convert to map - for (const item of items) { - var item_id: number; - const existing_item = await tx(tableName).select().where({ - name: item.toLowerCase() - }).first(); - - if (!existing_item) { - item_id = await (await insertToTable(item, tableName)(tx)).id; - } else - item_id = existing_item.id - - item_ids.push(item_id); - } + //TODO Convert to map + for (const item of items) { + var item_id: number; + const existing_item = await tx(tableName) + .select() + .where({ + name: item.toLowerCase(), + }) + .first(); + + if (!existing_item) { + item_id = await (await insertToTable(item, tableName)(tx)).id; + } else item_id = existing_item.id; + + item_ids.push(item_id); + } } catch (err) { - console.log("Failed to insert new item ", err) + console.log("Failed to insert new item ", err); } return item_ids; -}; + }; export const getOrCreateFederatedUser = ( - issuer: string, - username: string, - displayName: string, - done: CallableFunction + issuer: string, + username: string, + displayName: string, + done: CallableFunction ) => { - db.transaction(async tx => { - let user: User; - - - - try { - /** - * Do we already have a federated_credential ? - */ - const federated = await tx("federated_credentials").select().where({ - issuer, - subject: username, - }).first(); - - /** - * If not, create the `usr`, then the `federated_credential` - */ - if (!federated) { - user = await insertUserByDisplayName(displayName, username)(tx); - await insertFederatedCredential(user.id, issuer, username)(tx); - } else { - const user_ = await db.select().from("users").where({ - id: federated.id - }).first(); - - if (!user_) { - throw new Error( - `Unable to find matching user by \`federated_credential.id\`: ${federated.id - }` - ); - } - user = user_; - } + db.transaction(async (tx) => { + let user: User; - if(!user.getstream_token) { - const token = await generateGetStreamToken(user); - await updateUserGetStreamToken(user.id, token)(tx); - } - - done(null, user); - } catch (err) { - done(new Error( - "Failed to upsert federated authentication." - )); + try { + /** + * Do we already have a federated_credential ? + */ + const federated = await tx("federated_credentials") + .select() + .where({ + issuer, + subject: username, + }) + .first(); + + /** + * If not, create the `usr`, then the `federated_credential` + */ + if (!federated) { + user = await insertUserByDisplayName(displayName, username)(tx); + await insertFederatedCredential(user.id, issuer, username)(tx); + } else { + const user_ = await db + .select() + .from("users") + .where({ + id: federated.id, + }) + .first(); + + if (!user_) { + throw new Error( + `Unable to find matching user by \`federated_credential.id\`: ${federated.id}` + ); } - }); -}; + user = user_; + } -export const fetchFreelancerDetailsByUserID = (user_id: number | string) => - (tx: Knex.Transaction) => - fetchAllFreelancers()(tx) - .where({ user_id }) - .first() - .debug(false) - -export const fetchFreelancerDetailsByUsername = (username: string) => - (tx: Knex.Transaction) => - fetchAllFreelancers()(tx) - .where({ username: username }) - .first() - .debug(false) - - -export const fetchAllFreelancers = () => - (tx: Knex.Transaction) => - - tx.select( - "freelancers.id", - "freelanced_before", - "freelancing_goal", - "work_type", - "education", - "experience", - "facebook_link", - "twitter_link", - "telegram_link", - "discord_link", - "title", - "bio", - "user_id", - "username", - "display_name", - "freelancers.created", - tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), - tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), - - tx.raw("ARRAY_AGG(DISTINCT CAST(languages.name as text)) as languages"), - tx.raw("ARRAY_AGG(DISTINCT CAST(languages.id as text)) as language_ids"), - - tx.raw("ARRAY_AGG(DISTINCT CAST(services.name as text)) as services"), - tx.raw("ARRAY_AGG(DISTINCT CAST(services.id as text)) as service_ids"), - - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.name as text)) as clients"), - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_ids"), - - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.img as text)) as client_images"), - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_image_ids"), - tx.raw("(SUM(freelancer_ratings.rating) / COUNT(freelancer_ratings.rating)) as rating"), - tx.raw("COUNT(freelancer_ratings.rating) as num_ratings"), - - ).from("freelancers") - // Join services and many to many - .leftJoin("freelancer_services", { 'freelancers.id': "freelancer_services.freelancer_id" }) - .leftJoin("services", { 'freelancer_services.service_id': "services.id" }) - // Join clients and many to many - .leftJoin("freelancer_clients", { 'freelancers.id': "freelancer_clients.freelancer_id" }) - .leftJoin("clients", { 'freelancer_clients.client_id': "clients.id" }) - // Join skills and many to many - .leftJoin("freelancer_skills", { 'freelancers.id': "freelancer_skills.freelancer_id" }) - .leftJoin("skills", { 'freelancer_skills.skill_id': "skills.id" }) - // Join languages and many to many - .leftJoin("freelancer_languages", { 'freelancers.id': "freelancer_languages.freelancer_id" }) - .leftJoin("languages", { 'freelancer_languages.language_id': "languages.id" }) - .innerJoin("users", { "freelancers.user_id": "users.id" }) - .leftJoin("freelancer_ratings", { "freelancers.id": "freelancer_ratings.freelancer_id" }) - - // order and group by many-many selects - .orderBy("freelancers.created", "desc") - .groupBy("freelancers.id") - .groupBy("users.username") - .groupBy("users.display_name") - .orderBy("freelancers.id", "desc") - // TODO Add limit until we have spinning loading icon in freelancers page - .limit(100) - - -export const insertFreelancerDetails = ( - f: Freelancer, skill_ids: number[], - language_ids: number[], client_ids: number[], - service_ids: number[]) => - async (tx: Knex.Transaction) => - await tx("freelancers").insert( - { - freelanced_before: f.freelanced_before.toString(), - freelancing_goal: f.freelancing_goal, - work_type: f.work_type, - education: f.education, - experience: f.experience, - title: f.title, - bio: f.bio, - facebook_link: f.facebook_link, - twitter_link: f.twitter_link, - telegram_link: f.telegram_link, - discord_link: f.discord_link, - user_id: f.user_id - }) + if (!user.getstream_token) { + const token = await generateGetStreamToken(user); + await updateUserGetStreamToken(user.id, token)(tx); + } - .returning("id") - .then(ids => { - if (skill_ids) { - skill_ids.forEach(async (skillId) => { - if (skillId) { - await tx("freelancer_skills") - .insert({ - freelancer_id: ids[0], - skill_id: skillId - }) - } - - }) - } - - if (language_ids) { - language_ids.forEach(async (langId) => { - if (langId) { - await tx("freelancer_languages") - .insert({ - freelancer_id: ids[0], - language_id: langId - }) - } - }) - } - - if (client_ids) { - client_ids.forEach(async (clientId) => { - if (clientId) { - await tx("freelancer_clients") - .insert({ - freelancer_id: ids[0], - client_id: clientId - }) - } - }) - } - - if (service_ids) { - service_ids.forEach(async (serviceId) => { - if (serviceId) { - await tx("freelancer_services") - .insert({ - freelancer_id: ids[0], - service_id: serviceId - }) - } - }) - } - - return ids[0] - }) + done(null, user); + } catch (err) { + done(new Error("Failed to upsert federated authentication.")); + } + }); +}; +export const fetchFreelancerDetailsByUserID = + (user_id: number | string) => (tx: Knex.Transaction) => + fetchAllFreelancers()(tx).where({ user_id }).first().debug(false); + +export const fetchFreelancerDetailsByUsername = + (username: string) => (tx: Knex.Transaction) => + fetchAllFreelancers()(tx) + .where({ username: username }) + .first() + .debug(false); + +export const fetchAllFreelancers = () => (tx: Knex.Transaction) => + tx + .select( + "freelancers.id", + "freelanced_before", + "freelancing_goal", + "work_type", + "education", + "experience", + "facebook_link", + "twitter_link", + "telegram_link", + "discord_link", + "title", + "bio", + "user_id", + "username", + "display_name", + "freelancers.created", + tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), + tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), + + tx.raw("ARRAY_AGG(DISTINCT CAST(languages.name as text)) as languages"), + tx.raw("ARRAY_AGG(DISTINCT CAST(languages.id as text)) as language_ids"), + + tx.raw("ARRAY_AGG(DISTINCT CAST(services.name as text)) as services"), + tx.raw("ARRAY_AGG(DISTINCT CAST(services.id as text)) as service_ids"), + + tx.raw("ARRAY_AGG(DISTINCT CAST(clients.name as text)) as clients"), + tx.raw("ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_ids"), + + tx.raw("ARRAY_AGG(DISTINCT CAST(clients.img as text)) as client_images"), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_image_ids" + ), + tx.raw( + "(SUM(freelancer_ratings.rating) / COUNT(freelancer_ratings.rating)) as rating" + ), + tx.raw("COUNT(freelancer_ratings.rating) as num_ratings") + ) + .from("freelancers") + // Join services and many to many + .leftJoin("freelancer_services", { + "freelancers.id": "freelancer_services.freelancer_id", + }) + .leftJoin("services", { + "freelancer_services.service_id": "services.id", + }) + // Join clients and many to many + .leftJoin("freelancer_clients", { + "freelancers.id": "freelancer_clients.freelancer_id", + }) + .leftJoin("clients", { "freelancer_clients.client_id": "clients.id" }) + // Join skills and many to many + .leftJoin("freelancer_skills", { + "freelancers.id": "freelancer_skills.freelancer_id", + }) + .leftJoin("skills", { "freelancer_skills.skill_id": "skills.id" }) + // Join languages and many to many + .leftJoin("freelancer_languages", { + "freelancers.id": "freelancer_languages.freelancer_id", + }) + .leftJoin("languages", { + "freelancer_languages.language_id": "languages.id", + }) + .innerJoin("users", { "freelancers.user_id": "users.id" }) + .leftJoin("freelancer_ratings", { + "freelancers.id": "freelancer_ratings.freelancer_id", + }) + + // order and group by many-many selects + .orderBy("freelancers.created", "desc") + .groupBy("freelancers.id") + .groupBy("users.username") + .groupBy("users.display_name") + .orderBy("freelancers.id", "desc") + // TODO Add limit until we have spinning loading icon in freelancers page + .limit(100); + +export const insertFreelancerDetails = + ( + f: Freelancer, + skill_ids: number[], + language_ids: number[], + client_ids: number[], + service_ids: number[] + ) => + async (tx: Knex.Transaction) => + await tx("freelancers") + .insert({ + freelanced_before: f.freelanced_before.toString(), + freelancing_goal: f.freelancing_goal, + work_type: f.work_type, + education: f.education, + experience: f.experience, + title: f.title, + bio: f.bio, + facebook_link: f.facebook_link, + twitter_link: f.twitter_link, + telegram_link: f.telegram_link, + discord_link: f.discord_link, + user_id: f.user_id, + }) + + .returning("id") + .then((ids) => { + if (skill_ids) { + skill_ids.forEach(async (skillId) => { + if (skillId) { + await tx("freelancer_skills").insert({ + freelancer_id: ids[0], + skill_id: skillId, + }); + } + }); + } -export const updateFreelancerDetails = (userId: number, f: Freelancer) => - async (tx: Knex.Transaction) => ( - await tx("freelancers").update({ - freelanced_before: f.freelanced_before, - freelancing_goal: f.freelancing_goal, - work_type: f.work_type, - education: f.education, - experience: f.experience, - title: f.title, - bio: f.bio, - facebook_link: f.facebook_link, - twitter_link: f.twitter_link, - telegram_link: f.telegram_link, - discord_link: f.discord_link, - user_id: f.user_id - }) - .where({"user_id": userId}).returning("id") -) + if (language_ids) { + language_ids.forEach(async (langId) => { + if (langId) { + await tx("freelancer_languages").insert({ + freelancer_id: ids[0], + language_id: langId, + }); + } + }); + } + if (client_ids) { + client_ids.forEach(async (clientId) => { + if (clientId) { + await tx("freelancer_clients").insert({ + freelancer_id: ids[0], + client_id: clientId, + }); + } + }); + } + if (service_ids) { + service_ids.forEach(async (serviceId) => { + if (serviceId) { + await tx("freelancer_services").insert({ + freelancer_id: ids[0], + service_id: serviceId, + }); + } + }); + } + + return ids[0]; + }); + +export const updateFreelancerDetails = + (userId: number, f: Freelancer) => async (tx: Knex.Transaction) => + await tx("freelancers") + .update({ + freelanced_before: f.freelanced_before, + freelancing_goal: f.freelancing_goal, + work_type: f.work_type, + education: f.education, + experience: f.experience, + title: f.title, + bio: f.bio, + facebook_link: f.facebook_link, + twitter_link: f.twitter_link, + telegram_link: f.telegram_link, + discord_link: f.discord_link, + user_id: f.user_id, + }) + .where({ user_id: userId }) + .returning("id"); // The search briefs and all these lovely parameters. // Since we are using checkboxes only i unfortunatly ended up using all these parameters. // Because we could have multiple ranges of values and open ended ors. -export const searchBriefs = - async (tx: Knex.Transaction, filter: BriefSqlFilter) => - // select everything that is associated with brief. - fetchAllBriefs()(tx).where(function () { - if (filter.submitted_range.length > 0) { - this.whereBetween("users.briefs_submitted", [filter.submitted_range[0].toString(), Math.max(...filter.submitted_range).toString()]); - } - if (filter.submitted_is_max) { - this.orWhere('users.briefs_submitted', '>=', Math.max(...filter.submitted_range)) - } - }) - .where(function () { - if (filter.experience_range.length > 0) { - this.whereIn("experience_id", filter.experience_range) - } - }) - .where(function () { - if (filter.length_range.length > 0) { - this.whereIn("duration_id", filter.length_range) - } - if (filter.length_is_max) { - this.orWhere('duration_id', '>=', Math.max(...filter.length_range)) - } - }) - .where("headline", "ilike", "%" + filter.search_input + "%") - - - -export const searchFreelancers = - async (tx: Knex.Transaction, filter: FreelancerSqlFilter) => - fetchAllFreelancers()(tx) - .where(function () { - if (filter.skills_range.length > 0) { - this.whereIn('freelancer_skills.skill_id', filter.skills_range) - } - }) - .where(function () { - if (filter.services_range.length > 0) { - this.whereIn('freelancer_services.service_id', filter.services_range) - } - }) - .where(function () { - if (filter.languages_range.length > 0) { - this.whereIn('freelancer_languages.language_id', filter.languages_range) - } - }) - .where("username", "ilike", "%" + filter.search_input + "%") - .where("title", "ilike", "%" + filter.search_input + "%") - .where("bio", "ilike", "%" + filter.search_input + "%") - .debug(true) +export const searchBriefs = async ( + tx: Knex.Transaction, + filter: BriefSqlFilter +) => + // select everything that is associated with brief. + fetchAllBriefs()(tx) + .where(function () { + if (filter.submitted_range.length > 0) { + this.whereBetween("users.briefs_submitted", [ + filter.submitted_range[0].toString(), + Math.max(...filter.submitted_range).toString(), + ]); + } + if (filter.submitted_is_max) { + this.orWhere( + "users.briefs_submitted", + ">=", + Math.max(...filter.submitted_range) + ); + } + }) + .where(function () { + if (filter.experience_range.length > 0) { + this.whereIn("experience_id", filter.experience_range); + } + }) + .where(function () { + if (filter.length_range.length > 0) { + this.whereIn("duration_id", filter.length_range); + } + if (filter.length_is_max) { + this.orWhere("duration_id", ">=", Math.max(...filter.length_range)); + } + }) + .where("headline", "ilike", "%" + filter.search_input + "%"); + +export const searchFreelancers = async ( + tx: Knex.Transaction, + filter: FreelancerSqlFilter +) => + fetchAllFreelancers()(tx) + .where(function () { + if (filter.skills_range.length > 0) { + this.whereIn("freelancer_skills.skill_id", filter.skills_range); + } + }) + .where(function () { + if (filter.services_range.length > 0) { + this.whereIn("freelancer_services.service_id", filter.services_range); + } + }) + .where(function () { + if (filter.languages_range.length > 0) { + this.whereIn( + "freelancer_languages.language_id", + filter.languages_range + ); + } + }) + .where("username", "ilike", "%" + filter.search_input + "%") + .where("title", "ilike", "%" + filter.search_input + "%") + .where("bio", "ilike", "%" + filter.search_input + "%") + .debug(true); diff --git a/api/src/backend/routes/api/v1/briefs.ts b/api/src/backend/routes/api/v1/briefs.ts index 4a71aafe..13d570cf 100644 --- a/api/src/backend/routes/api/v1/briefs.ts +++ b/api/src/backend/routes/api/v1/briefs.ts @@ -1,132 +1,149 @@ import express, { response } from "express"; import db from "../../../db"; -import { fetchAllBriefs, insertBrief, upsertItems, searchBriefs, BriefSqlFilter, Brief, incrementUserBriefSubmissions, fetchBrief, fetchItems, fetchBriefApplications, fetchFreelancerDetailsByUserID, fetchProjectMilestones } from "../../../models"; +import { + fetchAllBriefs, + insertBrief, + upsertItems, + searchBriefs, + BriefSqlFilter, + Brief, + incrementUserBriefSubmissions, + fetchBrief, + fetchItems, + fetchBriefApplications, + fetchFreelancerDetailsByUserID, + fetchProjectMilestones, +} from "../../../models"; import { json } from "stream/consumers"; import { brotliDecompress } from "zlib"; -import { verifyUserIdFromJwt } from "../../../middleware/authentication/strategies/common" +import { verifyUserIdFromJwt } from "../../../middleware/authentication/strategies/common"; const router = express.Router(); router.get("/", async (req, res, next) => { - db.transaction(async (tx) => { - try { - await fetchAllBriefs()(tx).then(async (briefs: any) => { - await Promise.all([ - briefs, - ...briefs.map(async (brief: any) => { - brief.skills = await fetchItems(brief.skill_ids, "skills")(tx); - brief.industries = await fetchItems(brief.industry_ids, "skills")(tx); - }) - ]); - res.send(briefs); - }); - } catch (e) { - next(new Error( - `Failed to fetch all briefs`, - { cause: e as Error } - )); - } - }); + db.transaction(async (tx) => { + try { + await fetchAllBriefs()(tx).then(async (briefs: any) => { + await Promise.all([ + briefs, + ...briefs.map(async (brief: any) => { + brief.skills = await fetchItems(brief.skill_ids, "skills")(tx); + brief.industries = await fetchItems( + brief.industry_ids, + "skills" + )(tx); + }), + ]); + res.send(briefs); + }); + } catch (e) { + next(new Error(`Failed to fetch all briefs`, { cause: e as Error })); + } + }); }); router.get("/:id", (req, res, next) => { - const id = req.params.id; - db.transaction(async tx => { - try { - const brief = await fetchBrief(id)(tx); - await Promise.all([ - brief.skills = await fetchItems(brief.skill_ids, "skills")(tx), - brief.industries = await fetchItems(brief.industry_ids, "skills")(tx), - ]); - res.send(brief); - } catch (e) { - next(new Error( - `Failed to fetch brief with id ${id}`, - { cause: e as Error } - )); - } - }); + const id = req.params.id; + db.transaction(async (tx) => { + try { + const brief = await fetchBrief(id)(tx); + await Promise.all([ + (brief.skills = await fetchItems(brief.skill_ids, "skills")(tx)), + (brief.industries = await fetchItems(brief.industry_ids, "skills")(tx)), + ]); + res.send(brief); + } catch (e) { + next( + new Error(`Failed to fetch brief with id ${id}`, { + cause: e as Error, + }) + ); + } + }); }); router.get("/:id/applications", (req, res, next) => { - const id = req.params.id; - db.transaction(async tx => { - try { - const briefApplications = await fetchBriefApplications(id)(tx); + const id = req.params.id; + db.transaction(async (tx) => { + try { + const briefApplications = await fetchBriefApplications(id)(tx); - const response = await Promise.all(briefApplications.map(async (application) => { - return { - ...application, - freelancer: await fetchFreelancerDetailsByUserID(application.user_id)(tx), - milestones: await fetchProjectMilestones(application.id)(tx) - } - })); + const response = await Promise.all( + briefApplications.map(async (application) => { + return { + ...application, + freelancer: await fetchFreelancerDetailsByUserID( + application.user_id + )(tx), + milestones: await fetchProjectMilestones(application.id)(tx), + }; + }) + ); - res.send(response); - } catch (e) { - next(new Error( - `Failed to fetch brief applications with id ${id}`, - { cause: e as Error } - )); - } - }); + res.send(response); + } catch (e) { + next( + new Error(`Failed to fetch brief applications with id ${id}`, { + cause: e as Error, + }) + ); + } + }); }); router.post("/", (req, res, next) => { - db.transaction(async tx => { - const brief: Brief = req.body as Brief; - verifyUserIdFromJwt(req, res, next, brief.user_id) + db.transaction(async (tx) => { + const brief: Brief = req.body as Brief; + verifyUserIdFromJwt(req, res, next, brief.user_id); - try { - const skill_ids = await upsertItems(brief.skills, "skills")(tx); - const industry_ids = await upsertItems(brief.industries, "industries")(tx); - const brief_id = await insertBrief(brief, skill_ids, industry_ids, brief.scope_id, brief.duration_id)(tx); - if (!brief_id) { - return next(new Error( - "Failed to create brief." - )); - } - await incrementUserBriefSubmissions(brief.user_id)(tx); + try { + const skill_ids = await upsertItems(brief.skills, "skills")(tx); + const industry_ids = await upsertItems( + brief.industries, + "industries" + )(tx); + const brief_id = await insertBrief( + brief, + skill_ids, + industry_ids, + brief.scope_id, + brief.duration_id + )(tx); + if (!brief_id) { + return next(new Error("Failed to create brief.")); + } + await incrementUserBriefSubmissions(brief.user_id)(tx); - // Redirect to brief details page? - res.status(201).send( - { - status: "Successful", - brief_id: brief_id - } - ); - } catch (cause) { - next(new Error( - `Failed to insert brief .`, - { cause: cause as Error } - )); - } - }); + // Redirect to brief details page? + res.status(201).send({ + status: "Successful", + brief_id: brief_id, + }); + } catch (cause) { + next(new Error(`Failed to insert brief .`, { cause: cause as Error })); + } + }); }); router.post("/search", (req, res, next) => { - db.transaction(async tx => { - try { - const data: BriefSqlFilter = req.body; - const briefs: Array = await searchBriefs(tx, data); + db.transaction(async (tx) => { + try { + const data: BriefSqlFilter = req.body; + const briefs: Array = await searchBriefs(tx, data); - await Promise.all([ - briefs, - ...briefs.map(async (brief: any) => { - brief.skills = await fetchItems(brief.skill_ids, "skills")(tx); - brief.industries = await fetchItems(brief.industry_ids, "skills")(tx); - }) - ]); + await Promise.all([ + briefs, + ...briefs.map(async (brief: any) => { + brief.skills = await fetchItems(brief.skill_ids, "skills")(tx); + brief.industries = await fetchItems(brief.industry_ids, "skills")(tx); + }), + ]); - res.send(briefs); - } catch (e) { - next(new Error( - `Failed to search all briefs`, - { cause: e as Error } - )); - } - }); + res.send(briefs); + } catch (e) { + next(new Error(`Failed to search all briefs`, { cause: e as Error })); + } + }); }); - export default router; diff --git a/api/src/backend/routes/api/v1/freelancers.ts b/api/src/backend/routes/api/v1/freelancers.ts index f6216ce6..532f56ad 100644 --- a/api/src/backend/routes/api/v1/freelancers.ts +++ b/api/src/backend/routes/api/v1/freelancers.ts @@ -2,161 +2,218 @@ import express, { response } from "express"; import db from "../../../db"; import * as models from "../../../models"; import passport from "passport"; -import { upsertItems, fetchAllFreelancers, fetchItems, FreelancerSqlFilter, fetchFreelancerDetailsByUsername, updateFreelancerDetails, insertFreelancerDetails, searchFreelancers } from "../../../models"; -import { Freelancer } from "../../../models" -import { validateUserFromJwt, verifyUserIdFromJwt } from "../../../middleware/authentication/strategies/common"; +import { + upsertItems, + fetchAllFreelancers, + fetchItems, + FreelancerSqlFilter, + fetchFreelancerDetailsByUsername, + updateFreelancerDetails, + insertFreelancerDetails, + searchFreelancers, +} from "../../../models"; +import { Freelancer } from "../../../models"; +import { + validateUserFromJwt, + verifyUserIdFromJwt, +} from "../../../middleware/authentication/strategies/common"; const router = express.Router(); router.get("/", (req, res, next) => { - db.transaction(async tx => { - try { - await fetchAllFreelancers()(tx).then(async (freelancers: any) => { - await Promise.all([ - ...freelancers.map(async (freelancer: any) => { - freelancer.skills = await fetchItems(freelancer.skill_ids, "skills")(tx); - freelancer.client_images = await fetchItems(freelancer.client_ids, "clients")(tx); - freelancer.languages = await fetchItems(freelancer.language_ids, "languages")(tx); - freelancer.services = await fetchItems(freelancer.service_ids, "services")(tx); - }) - ]); - res.send(freelancers); - }); - - } catch (e) { - next(new Error( - `Failed to fetch all freelancers`, - { cause: e as Error } - )); - } - }); + db.transaction(async (tx) => { + try { + await fetchAllFreelancers()(tx).then(async (freelancers: any) => { + await Promise.all([ + ...freelancers.map(async (freelancer: any) => { + freelancer.skills = await fetchItems( + freelancer.skill_ids, + "skills" + )(tx); + freelancer.client_images = await fetchItems( + freelancer.client_ids, + "clients" + )(tx); + freelancer.languages = await fetchItems( + freelancer.language_ids, + "languages" + )(tx); + freelancer.services = await fetchItems( + freelancer.service_ids, + "services" + )(tx); + }), + ]); + res.send(freelancers); + }); + } catch (e) { + next( + new Error(`Failed to fetch all freelancers`, { + cause: e as Error, + }) + ); + } + }); }); - router.get("/:username", (req, res, next) => { - const username = req.params.username; - - db.transaction(async tx => { - try { - const freelancer = await fetchFreelancerDetailsByUsername(username)(tx); - - if (!freelancer) { - return res.status(404).end(); - } - - await Promise.all([ - freelancer.skills = await fetchItems(freelancer.skill_ids, "skills")(tx), - freelancer.client_images = await fetchItems(freelancer.client_ids, "clients")(tx), - freelancer.languages = await fetchItems(freelancer.language_ids, "languages")(tx), - freelancer.services = await fetchItems(freelancer.service_ids, "services")(tx), - ]); - - - // Used to show/hide edit buttons if the correct user. - if (validateUserFromJwt(req, res, next, freelancer.user_id)) { - res.cookie("isUser", true) - } else { - res.cookie("isUser", false) - } - - res.send(freelancer); - } catch (e) { - next(new Error( - `Failed to fetch freelancer details by userid: ${username}`, - { cause: e as Error } - )); - } - }); + const username = req.params.username; + + db.transaction(async (tx) => { + try { + const freelancer = await fetchFreelancerDetailsByUsername(username)(tx); + + if (!freelancer) { + return res.status(404).end(); + } + + await Promise.all([ + (freelancer.skills = await fetchItems( + freelancer.skill_ids, + "skills" + )(tx)), + (freelancer.client_images = await fetchItems( + freelancer.client_ids, + "clients" + )(tx)), + (freelancer.languages = await fetchItems( + freelancer.language_ids, + "languages" + )(tx)), + (freelancer.services = await fetchItems( + freelancer.service_ids, + "services" + )(tx)), + ]); + + // Used to show/hide edit buttons if the correct user. + if (validateUserFromJwt(req, res, next, freelancer.user_id)) { + res.cookie("isUser", true); + } else { + res.cookie("isUser", false); + } + + res.send(freelancer); + } catch (e) { + next( + new Error(`Failed to fetch freelancer details by userid: ${username}`, { + cause: e as Error, + }) + ); + } + }); }); router.post("/", (req, res, next) => { - const freelancer = req.body.freelancer as Freelancer; - verifyUserIdFromJwt(req, res, next, freelancer.user_id) - - db.transaction(async tx => { - try { - - const skill_ids = await upsertItems(freelancer.skills, "skills")(tx); - const language_ids = await upsertItems(freelancer.languages, "languages")(tx); - const services_ids = await upsertItems(freelancer.services, "services")(tx); - let client_ids: number[] = [] - - if (freelancer.clients) { - client_ids = await upsertItems(freelancer.clients, "services")(tx); - } - const freelancer_id = await insertFreelancerDetails( - freelancer, skill_ids, language_ids, client_ids, services_ids - )(tx); - - if (!freelancer_id) { - return next(new Error( - "Failed to insert freelancer details." - )); - } - - res.status(201).send( - { - status: "Successful", - freelancer_id: freelancer_id - } - ); - } catch (cause) { - next(new Error( - `Failed to insert freelancer details .`, - { cause: cause as Error } - )); - } - }); + const freelancer = req.body.freelancer as Freelancer; + verifyUserIdFromJwt(req, res, next, freelancer.user_id); + + db.transaction(async (tx) => { + try { + const skill_ids = await upsertItems(freelancer.skills, "skills")(tx); + const language_ids = await upsertItems( + freelancer.languages, + "languages" + )(tx); + const services_ids = await upsertItems( + freelancer.services, + "services" + )(tx); + let client_ids: number[] = []; + + if (freelancer.clients) { + client_ids = await upsertItems(freelancer.clients, "services")(tx); + } + const freelancer_id = await insertFreelancerDetails( + freelancer, + skill_ids, + language_ids, + client_ids, + services_ids + )(tx); + + if (!freelancer_id) { + return next(new Error("Failed to insert freelancer details.")); + } + + res.status(201).send({ + status: "Successful", + freelancer_id: freelancer_id, + }); + } catch (cause) { + next( + new Error(`Failed to insert freelancer details .`, { + cause: cause as Error, + }) + ); + } + }); }); router.put("/:username", async (req, res, next) => { - const username = req.params.username; - const freelancer = req.body.freelancer as Freelancer; - verifyUserIdFromJwt(req, res, next, freelancer.user_id) - - db.transaction(async tx => { - - try { - const exists: any = await models.fetchFreelancerDetailsByUsername(username)(tx); - if (!exists) { - return next(new Error( - "Freelancer does not exist." - )); - } - await models.updateFreelancerDetails(exists.user_id, freelancer)(tx); - let updated_freelancer_details = await models.fetchFreelancerDetailsByUsername(username)(tx); - return res.send(updated_freelancer_details); - } catch (e: any) { - return next(new Error( - `Failed to update freelancer details: ${e.message}`, - )); - } - }); + const username = req.params.username; + const freelancer = req.body.freelancer as Freelancer; + verifyUserIdFromJwt(req, res, next, freelancer.user_id); + + db.transaction(async (tx) => { + try { + const exists: any = await models.fetchFreelancerDetailsByUsername( + username + )(tx); + if (!exists) { + return next(new Error("Freelancer does not exist.")); + } + await models.updateFreelancerDetails(exists.user_id, freelancer)(tx); + let updated_freelancer_details = + await models.fetchFreelancerDetailsByUsername(username)(tx); + return res.send(updated_freelancer_details); + } catch (e: any) { + return next( + new Error(`Failed to update freelancer details: ${e.message}`) + ); + } + }); }); router.post("/search", (req, res, next) => { - db.transaction(async tx => { - try { - const filter: FreelancerSqlFilter = req.body; - console.log(filter); - const freelancers: Array = await searchFreelancers(tx, filter); - await Promise.all([ - ...freelancers.map(async (freelancer: any) => { - freelancer.skills = await fetchItems(freelancer.skill_ids, "skills")(tx); - freelancer.client_images = await fetchItems(freelancer.client_ids, "clients")(tx); - freelancer.languages = await fetchItems(freelancer.language_ids, "languages")(tx); - freelancer.services = await fetchItems(freelancer.service_ids, "services")(tx); - }) - ]); - - res.send(freelancers); - } catch (e) { - next(new Error( - `Failed to search all freelancers`, - { cause: e as Error } - )); - } - }); + db.transaction(async (tx) => { + try { + const filter: FreelancerSqlFilter = req.body; + console.log(filter); + const freelancers: Array = await searchFreelancers( + tx, + filter + ); + await Promise.all([ + ...freelancers.map(async (freelancer: any) => { + freelancer.skills = await fetchItems( + freelancer.skill_ids, + "skills" + )(tx); + freelancer.client_images = await fetchItems( + freelancer.client_ids, + "clients" + )(tx); + freelancer.languages = await fetchItems( + freelancer.language_ids, + "languages" + )(tx); + freelancer.services = await fetchItems( + freelancer.service_ids, + "services" + )(tx); + }), + ]); + + res.send(freelancers); + } catch (e) { + next( + new Error(`Failed to search all freelancers`, { + cause: e as Error, + }) + ); + } + }); }); export default router; diff --git a/api/src/backend/routes/api/v1/index.ts b/api/src/backend/routes/api/v1/index.ts index 034e5911..e7d7c4c5 100644 --- a/api/src/backend/routes/api/v1/index.ts +++ b/api/src/backend/routes/api/v1/index.ts @@ -10,18 +10,18 @@ import freelancersRouter from "./freelancers"; const router = express.Router(); router.get( - "/user", - passport.authenticate("jwt", { session: false }), - (req, res) => { - res.send(req.user); - } + "/user", + passport.authenticate("jwt", { session: false }), + (req, res) => { + res.send(req.user); + } ); router.get("/info", (req, res) => { - res.send({ - imbueNetworkWebsockAddr: config.imbueNetworkWebsockAddr, - relayChainWebsockAddr: config.relayChainWebsockAddr, - }); + res.send({ + imbueNetworkWebsockAddr: config.imbueNetworkWebsockAddr, + relayChainWebsockAddr: config.relayChainWebsockAddr, + }); }); router.use("/projects", projectsRouter); diff --git a/api/src/backend/routes/api/v1/milestones.ts b/api/src/backend/routes/api/v1/milestones.ts index b95ef6c4..85222a68 100644 --- a/api/src/backend/routes/api/v1/milestones.ts +++ b/api/src/backend/routes/api/v1/milestones.ts @@ -2,123 +2,126 @@ import passport from "passport"; import * as models from "../../../models"; import db from "../../../db"; import express from "express"; -import {updateMilestoneDetails} from "../../../models"; +import { updateMilestoneDetails } from "../../../models"; const router = express.Router(); - -router.put("/:id/milestone/:milestoneId", passport.authenticate('jwt', { session: false }),(req, res, next) => { +router.put( + "/:id/milestone/:milestoneId", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { const id = req.params.id; const milestoneId = req.params.milestoneId; const details = req.body.details; + if (details != null) { + db.transaction(async (tx) => { + try { + // ensure the project exists first + const exists = await models.fetchAllMilestone(id)(tx); - if(details!=null) - { - db.transaction(async tx => { - try { - // ensure the project exists first - const exists = await models.fetchAllMilestone(id)(tx); - - if (!exists) { - return res.status(404).end(); - } + if (!exists) { + return res.status(404).end(); + } - const updated = await models.updateMilestoneDetails(id,milestoneId,details)(tx); + const updated = await models.updateMilestoneDetails( + id, + milestoneId, + details + )(tx); - res.status(200).send( - updated - ); - } catch (cause) { - next(new Error( - `Failed to update milestones`, - {cause: cause as Error} - )); - } - }); - } - else { - res.status(400).send( - {message: "details not found "} - ); + res.status(200).send(updated); + } catch (cause) { + next( + new Error(`Failed to update milestones`, { + cause: cause as Error, + }) + ); + } + }); + } else { + res.status(400).send({ message: "details not found " }); } - - -}); + } +); router.get("/:id/milestones", (req, res, next) => { - const id = req.params.id; - db.transaction(async tx => { - try { - const projects = await models.fetchAllMilestone(id)(tx); - res.send(projects); - } catch (e) { - next(new Error( - `Failed to fetch all milestones`, - {cause: e as Error} - )); - } - }); + const id = req.params.id; + db.transaction(async (tx) => { + try { + const projects = await models.fetchAllMilestone(id)(tx); + res.send(projects); + } catch (e) { + next( + new Error(`Failed to fetch all milestones`, { + cause: e as Error, + }) + ); + } + }); }); - router.get("/:id/milestone/:milestoneId", (req, res, next) => { - const id = req.params.id; - const milestoneId = req.params.milestoneId; - db.transaction(async tx => { - try { - const milestone = await models.fetchMilestoneByIndex(id,milestoneId)(tx); - res.send(milestone); - } catch (e) { - next(new Error( - `Failed to fetch the milestone`, - {cause: e as Error} - )); - } - }); + const id = req.params.id; + const milestoneId = req.params.milestoneId; + db.transaction(async (tx) => { + try { + const milestone = await models.fetchMilestoneByIndex(id, milestoneId)(tx); + res.send(milestone); + } catch (e) { + next( + new Error(`Failed to fetch the milestone`, { + cause: e as Error, + }) + ); + } + }); }); - -router.post("/", passport.authenticate('jwt', { session: false }),(req, res, next) => { +router.post( + "/", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { try { - validateMilestone(req.body.milestoneDetails); + validateMilestone(req.body.milestoneDetails); } catch (e) { - res.status(400).send( - {message: (e as Error).message} - ); + res.status(400).send({ message: (e as Error).message }); } - db.transaction(async tx => { - try { - const project = await models.insertMilestoneDetails( - req.body.milestoneDetails - )(tx); - - res.status(201).send(project); - } catch (cause) { - next(new Error( - `Failed to insert milestone.`, - {cause: cause as Error} - )); - } + db.transaction(async (tx) => { + try { + const project = await models.insertMilestoneDetails( + req.body.milestoneDetails + )(tx); + + res.status(201).send(project); + } catch (cause) { + next( + new Error(`Failed to insert milestone.`, { + cause: cause as Error, + }) + ); + } }); -}); + } +); const validateMilestone = (milestoneDetails: models.MilestoneDetails) => { - if (!validateMilestone) { - throw new Error("Missing `milestone` entry."); - } - - const entries = Object.entries(milestoneDetails); - if (entries.filter(([_,v]) => { - // undefined not allowed - return v === void 0; + if (!validateMilestone) { + throw new Error("Missing `milestone` entry."); + } + + const entries = Object.entries(milestoneDetails); + if ( + entries.filter(([_, v]) => { + // undefined not allowed + return v === void 0; }).length - ) { - throw new Error( - `Project milestone entries can't have a value of \`undefined\`.` - ); - } -} + ) { + throw new Error( + `Project milestone entries can't have a value of \`undefined\`.` + ); + } +}; export default router; diff --git a/api/src/backend/routes/api/v1/projects.ts b/api/src/backend/routes/api/v1/projects.ts index 9a4c856d..8cc1ab2b 100644 --- a/api/src/backend/routes/api/v1/projects.ts +++ b/api/src/backend/routes/api/v1/projects.ts @@ -3,10 +3,9 @@ import db from "../../../db"; import * as models from "../../../models"; import passport from "passport"; - type ProjectPkg = models.Project & { - milestones: models.Milestone[] -} + milestones: models.Milestone[]; +}; /** * FIXME: all of this is terriblme @@ -15,255 +14,261 @@ type ProjectPkg = models.Project & { const router = express.Router(); router.get("/", (req, res, next) => { - db.transaction(async tx => { - try { - const projects = await models.fetchAllProjects()(tx); - res.send(projects); - } catch (e) { - next(new Error( - `Failed to fetch all projects`, - {cause: e as Error} - )); - } - }); + db.transaction(async (tx) => { + try { + const projects = await models.fetchAllProjects()(tx); + res.send(projects); + } catch (e) { + next(new Error(`Failed to fetch all projects`, { cause: e as Error })); + } + }); }); - router.get("/:id", (req, res, next) => { - const id = req.params.id; + const id = req.params.id; - db.transaction(async tx => { - try { - const project = await models.fetchProject(id)(tx); - - if (!project) { - return res.status(404).end(); - } - - const pkg: ProjectPkg = { - ...project, - milestones: await models.fetchProjectMilestones(id)(tx) - }; - - res.send(pkg); - } catch (e) { - next(new Error( - `Failed to fetch project by id: ${id}`, - {cause: e as Error} - )); - } - }); + db.transaction(async (tx) => { + try { + const project = await models.fetchProject(id)(tx); + + if (!project) { + return res.status(404).end(); + } + + const pkg: ProjectPkg = { + ...project, + milestones: await models.fetchProjectMilestones(id)(tx), + }; + + res.send(pkg); + } catch (e) { + next( + new Error(`Failed to fetch project by id: ${id}`, { + cause: e as Error, + }) + ); + } + }); }); -router.post("/", passport.authenticate('jwt', { session: false }), (req, res, next) => { +router.post( + "/", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { const { - name, - logo, - description, - website, - category, - required_funds, - currency_id, - owner, - milestones, - brief_id, - total_cost_without_fee, - imbue_fee, + name, + logo, + description, + website, + category, + required_funds, + currency_id, + owner, + milestones, + brief_id, + total_cost_without_fee, + imbue_fee, } = req.body; - - db.transaction(async tx => { - try { - const project = await models.insertProject({ - name, - logo, - description, - website, - category, - required_funds, - currency_id, - owner, - user_id: (req.user as any).id, - brief_id, - total_cost_without_fee, - imbue_fee, - })(tx); - - if (!project.id) { - return next(new Error( - "Failed to insert milestones: `project_id` missing." - )); - } - - const pkg: ProjectPkg = { - ...project, - milestones: await models.insertMilestones( - milestones, - project.id, - )(tx) - } - - res.status(201).send(pkg); - } catch (cause) { - next(new Error( - `Failed to insert project.`, - {cause: cause as Error} - )); + + db.transaction(async (tx) => { + try { + const project = await models.insertProject({ + name, + logo, + description, + website, + category, + required_funds, + currency_id, + owner, + user_id: (req.user as any).id, + brief_id, + total_cost_without_fee, + imbue_fee, + })(tx); + + if (!project.id) { + return next( + new Error("Failed to insert milestones: `project_id` missing.") + ); } + + const pkg: ProjectPkg = { + ...project, + milestones: await models.insertMilestones(milestones, project.id)(tx), + }; + + res.status(201).send(pkg); + } catch (cause) { + next( + new Error(`Failed to insert project.`, { + cause: cause as Error, + }) + ); + } }); -}); + } +); -router.put("/:id", passport.authenticate('jwt', { session: false }), (req, res, next) => { +router.put( + "/:id", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { const id = req.params.id; const { - name, - logo, - description, - website, - category, - required_funds, - currency_id, - chain_project_id, - owner, - milestones, - total_cost_without_fee, - imbue_fee, + name, + logo, + description, + website, + category, + required_funds, + currency_id, + chain_project_id, + owner, + milestones, + total_cost_without_fee, + imbue_fee, } = req.body; - const user_id = (req.user as any).id; - db.transaction(async tx => { - try { - // ensure the project exists first - const exists = await models.fetchProject(id)(tx); - - if (!exists) { - return res.status(404).end(); - } - - if (exists.user_id !== user_id) { - return res.status(403).end(); - } - - const project = await models.updateProject(id, { - name, - logo, - description, - website, - category, - chain_project_id, - required_funds, - currency_id, - owner, - total_cost_without_fee, - imbue_fee - })(tx); - - if (!project.id) { - return next(new Error( - "Cannot update milestones: `project_id` missing." - )); - } - - // drop then recreate - await models.deleteMilestones(id)(tx); - - const pkg: ProjectPkg = { - ...project, - milestones: await models.insertMilestones( - milestones, - project.id, - )(tx) - } - - res.status(200).send(pkg); - } catch (cause) { - next(new Error( - `Failed to update project.`, - {cause: cause as Error} - )); + db.transaction(async (tx) => { + try { + // ensure the project exists first + const exists = await models.fetchProject(id)(tx); + + if (!exists) { + return res.status(404).end(); } - }); -}); -const validateProperties = (properties: models.ProjectProperties) => { - if (!properties) { - throw new Error("Missing `properties` entry."); - } + if (exists.user_id !== user_id) { + return res.status(403).end(); + } + + const project = await models.updateProject(id, { + name, + logo, + description, + website, + category, + chain_project_id, + required_funds, + currency_id, + owner, + total_cost_without_fee, + imbue_fee, + })(tx); + + if (!project.id) { + return next( + new Error("Cannot update milestones: `project_id` missing.") + ); + } - const entries = Object.entries(properties); - if (entries.filter(([_,v]) => { - // undefined not allowed - return v === void 0; - }).length - ) { - throw new Error( - `Project property entries can't have a value of \`undefined\`.` + // drop then recreate + await models.deleteMilestones(id)(tx); + + const pkg: ProjectPkg = { + ...project, + milestones: await models.insertMilestones(milestones, project.id)(tx), + }; + + res.status(200).send(pkg); + } catch (cause) { + next( + new Error(`Failed to update project.`, { + cause: cause as Error, + }) ); - } -} + } + }); + } +); -router.get('/:id/properties', passport.authenticate('jwt', { session: false }), (req, res, next) => { +const validateProperties = (properties: models.ProjectProperties) => { + if (!properties) { + throw new Error("Missing `properties` entry."); + } + + const entries = Object.entries(properties); + if ( + entries.filter(([_, v]) => { + // undefined not allowed + return v === void 0; + }).length + ) { + throw new Error( + `Project property entries can't have a value of \`undefined\`.` + ); + } +}; + +router.get( + "/:id/properties", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { const id = req.params.id; - db.transaction(async tx => { - try { - const project = await models.fetchProjectWithProperties(id)(tx); - - if (!project) { - return res.status(404).end(); - } + db.transaction(async (tx) => { + try { + const project = await models.fetchProjectWithProperties(id)(tx); - res.send(project); - } catch (e) { - next(new Error( - `Failed to fetch project by id: ${id}`, - {cause: e as Error} - )); + if (!project) { + return res.status(404).end(); } + + res.send(project); + } catch (e) { + next( + new Error(`Failed to fetch project by id: ${id}`, { + cause: e as Error, + }) + ); + } }); -}); + } +); -router.put("/:id/properties", passport.authenticate('jwt', { session: false }), (req, res, next) => { +router.put( + "/:id/properties", + passport.authenticate("jwt", { session: false }), + (req, res, next) => { const id = req.params.id; try { - validateProperties({ ...req.body.properties, project_id: id }); + validateProperties({ ...req.body.properties, project_id: id }); } catch (e) { - res.status(400).send( - {message: (e as Error).message} - ); + res.status(400).send({ message: (e as Error).message }); } - const { - faq - } = req.body.properties as models.ProjectProperties; - - db.transaction(async tx => { - try { - // ensure the project exists first - const exists = await models.fetchProject(id)(tx); - - if (!exists) { - return res.status(404).end(); - } - - const updated = await models.updateProjectProperties(id, { - faq - } as models.ProjectProperties)(tx); - - res.status(200).send({ - ...exists, - properties: updated - }); - } catch (cause) { - next(new Error( - `Failed to update project properties.`, - {cause: cause as Error} - )); + const { faq } = req.body.properties as models.ProjectProperties; + + db.transaction(async (tx) => { + try { + // ensure the project exists first + const exists = await models.fetchProject(id)(tx); + + if (!exists) { + return res.status(404).end(); } - }); -}); + const updated = await models.updateProjectProperties(id, { + faq, + } as models.ProjectProperties)(tx); + + res.status(200).send({ + ...exists, + properties: updated, + }); + } catch (cause) { + next( + new Error(`Failed to update project properties.`, { + cause: cause as Error, + }) + ); + } + }); + } +); export default router; diff --git a/api/src/backend/routes/api/v1/users.ts b/api/src/backend/routes/api/v1/users.ts index 95ff0efb..e7eafa61 100644 --- a/api/src/backend/routes/api/v1/users.ts +++ b/api/src/backend/routes/api/v1/users.ts @@ -1,184 +1,202 @@ import express, { response } from "express"; import db from "../../../db"; import * as models from "../../../models"; -import { User, getOrCreateFederatedUser, updateFederatedLoginUser, fetchUserBriefs, fetchBriefApplications, fetchBriefProjects, fetchProjectMilestones } from "../../../models"; +import { + User, + getOrCreateFederatedUser, + updateFederatedLoginUser, + fetchUserBriefs, + fetchBriefApplications, + fetchBriefProjects, + fetchProjectMilestones, +} from "../../../models"; // @ts-ignore -import * as passportJwt from "passport-jwt" +import * as passportJwt from "passport-jwt"; // @ts-ignore -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; const router = express.Router(); type ProjectPkg = models.Project & { - milestones: models.Milestone[] -} + milestones: models.Milestone[]; +}; router.get("/:id/project", (req, res, next) => { - const id = req.params.id; - - db.transaction(async tx => { - try { - const project = await models.fetchUserProject(id)(tx); - if (!project) { - return res.status(404).end(); - } - - const pkg: ProjectPkg = { - ...project, - milestones: await models.fetchProjectMilestones(id)(tx) - }; - - res.send(pkg); - } catch (e) { - next(new Error( - `Failed to fetch projects for user id: ${id}`, - { cause: e as Error } - )); - } - }); + const id = req.params.id; + + db.transaction(async (tx) => { + try { + const project = await models.fetchUserProject(id)(tx); + if (!project) { + return res.status(404).end(); + } + + const pkg: ProjectPkg = { + ...project, + milestones: await models.fetchProjectMilestones(id)(tx), + }; + + res.send(pkg); + } catch (e) { + next( + new Error(`Failed to fetch projects for user id: ${id}`, { + cause: e as Error, + }) + ); + } + }); }); router.get("/:id/briefs/:briefId", (req, res, next) => { - const id = req.params.id; - const briefId = req.params.briefId; - db.transaction(async tx => { - try { - const project = await models.fetchUserBriefApplications(id,briefId)(tx); - - if (!project) { - return res.status(404).end(); - } - - const pkg: ProjectPkg = { - ...project, - milestones: await models.fetchProjectMilestones(Number(project.id))(tx) - }; - - res.send(pkg); - } catch (e) { - next(new Error( - `Failed to fetch project by id: ${id}`, - {cause: e as Error} - )); - } - }); + const id = req.params.id; + const briefId = req.params.briefId; + db.transaction(async (tx) => { + try { + const project = await models.fetchUserBriefApplications(id, briefId)(tx); + + if (!project) { + return res.status(404).end(); + } + + const pkg: ProjectPkg = { + ...project, + milestones: await models.fetchProjectMilestones(Number(project.id))(tx), + }; + + res.send(pkg); + } catch (e) { + next( + new Error(`Failed to fetch project by id: ${id}`, { + cause: e as Error, + }) + ); + } + }); }); router.get("/:id/briefs", (req, res, next) => { - const id = req.params.id; - db.transaction(async tx => { - try { - const briefs = await fetchUserBriefs(id)(tx); - const pendingReviewBriefs = briefs.filter(m => m.project_id == null); - const briefsWithProjects = briefs.filter(m => m.project_id !== null); - - const briefsUnderReview = await Promise.all(pendingReviewBriefs.map(async (brief) => { - return { - ...brief, - number_of_applications: (await fetchBriefApplications(brief.id)(tx)).length - } - })); - - const acceptedBriefs = await Promise.all(briefsWithProjects.map(async (brief) => { - return { - ...brief, - project: (await models.fetchProject(brief.project_id)(tx)), - milestones: (await fetchProjectMilestones(brief.project_id)(tx)) - } - })); - - const response = { - briefsUnderReview, - acceptedBriefs, - } - - res.send(response); - } catch (e) { - next(new Error( - `Failed to fetch all briefs for user id ${id}`, - { cause: e as Error } - )); - } - }); + const id = req.params.id; + db.transaction(async (tx) => { + try { + const briefs = await fetchUserBriefs(id)(tx); + const pendingReviewBriefs = briefs.filter((m) => m.project_id == null); + const briefsWithProjects = briefs.filter((m) => m.project_id !== null); + + const briefsUnderReview = await Promise.all( + pendingReviewBriefs.map(async (brief) => { + return { + ...brief, + number_of_applications: (await fetchBriefApplications(brief.id)(tx)) + .length, + }; + }) + ); + + const acceptedBriefs = await Promise.all( + briefsWithProjects.map(async (brief) => { + return { + ...brief, + project: await models.fetchProject(brief.project_id)(tx), + milestones: await fetchProjectMilestones(brief.project_id)(tx), + }; + }) + ); + + const response = { + briefsUnderReview, + acceptedBriefs, + }; + + res.send(response); + } catch (e) { + next( + new Error(`Failed to fetch all briefs for user id ${id}`, { + cause: e as Error, + }) + ); + } + }); }); router.post("/", (req, res, next) => { - const { - username, - email, - password - } = req.body; - - let updateUserDetails = async (err: Error, user: User) => { - if (err) { - next(err); - } - - if (!user) { - next(new Error("No user provided.")); - } - - db.transaction(async tx => { - try { - const updatedUser = await updateFederatedLoginUser( - user, username, email, password - )(tx); - - res.send(updatedUser); - } catch (e) { - tx.rollback(); - next(new Error( - `Unable to upsert details for user: ${username}`, - { cause: e as Error } - )); - } - }); - }; - - getOrCreateFederatedUser( - "Imbue Network", - email, - username, - updateUserDetails); + const { username, email, password } = req.body; + + let updateUserDetails = async (err: Error, user: User) => { + if (err) { + next(err); + } + + if (!user) { + next(new Error("No user provided.")); + } + + db.transaction(async (tx) => { + try { + const updatedUser = await updateFederatedLoginUser( + user, + username, + email, + password + )(tx); + + res.send(updatedUser); + } catch (e) { + tx.rollback(); + next( + new Error(`Unable to upsert details for user: ${username}`, { + cause: e as Error, + }) + ); + } + }); + }; + getOrCreateFederatedUser("Imbue Network", email, username, updateUserDetails); }); - router.get("/:userOrEmail", (req, res, next) => { - const userOrEmail = req.params.userOrEmail; - db.transaction(async tx => { - try { - const user: User = await models.fetchUserOrEmail(userOrEmail)(tx) as User; - if (!user) { - return res.status(404).end(); - } - res.send({id: user.id, display_name: user.display_name, username: user.username}); - } catch (e) { - next(new Error( - `Failed to fetch user ${userOrEmail}`, - { cause: e as Error } - )); - } - }); + const userOrEmail = req.params.userOrEmail; + db.transaction(async (tx) => { + try { + const user: User = (await models.fetchUserOrEmail(userOrEmail)( + tx + )) as User; + if (!user) { + return res.status(404).end(); + } + res.send({ + id: user.id, + display_name: user.display_name, + username: user.username, + }); + } catch (e) { + next( + new Error(`Failed to fetch user ${userOrEmail}`, { + cause: e as Error, + }) + ); + } + }); }); router.get("/byid/:id", (req, res, next) => { - const id = Number(req.params.id); - db.transaction(async tx => { - try { - const user: User = await models.fetchUser(id)(tx) as User; - if (!user) { - return res.status(404).end(); - } - res.send({id: user.id, display_name: user.display_name, username: user.username}); - } catch (e) { - next(new Error( - `Failed to fetch user ${id}`, - { cause: e as Error } - )); - } - }); + const id = Number(req.params.id); + db.transaction(async (tx) => { + try { + const user: User = (await models.fetchUser(id)(tx)) as User; + if (!user) { + return res.status(404).end(); + } + res.send({ + id: user.id, + display_name: user.display_name, + username: user.username, + }); + } catch (e) { + next(new Error(`Failed to fetch user ${id}`, { cause: e as Error })); + } + }); }); export default router; diff --git a/api/src/backend/routes/web/index.ts b/api/src/backend/routes/web/index.ts index 055d1b3c..988476ea 100644 --- a/api/src/backend/routes/web/index.ts +++ b/api/src/backend/routes/web/index.ts @@ -4,112 +4,116 @@ import passport from "passport"; const router = express.Router(); router.get( - "/dashboard", - passport.authenticate("jwt", { - session: false, - failureRedirect: "/dapp/login?redirect=/dapp/dashboard", - }), - (req, res) => { - res.render("dashboard"); - } + "/dashboard", + passport.authenticate("jwt", { + session: false, + failureRedirect: "/dapp/login?redirect=/dapp/dashboard", + }), + (req, res) => { + res.render("dashboard"); + } ); router.get("/login", (req, res) => { - res.render("login"); + res.render("login"); }); router.get("/", (req, res) => { - res.render("proposals"); + res.render("proposals"); }); router.get("/proposals", (req, res) => { - res.render("proposals"); + res.render("proposals"); }); router.get("/proposals/new-details", (req, res) => { - res.render("details"); + res.render("details"); }); router.get("/projects/new", (req, res) => { - res.render("new-project"); + res.render("new-project"); }); router.get("/proposals/:projectId", (req, res) => { - res.render("details"); + res.render("details"); }); router.get("/briefs", (req, res) => { - res.render("briefs"); + res.render("briefs"); }); router.get( - "/briefs/new", - passport.authenticate("jwt", { - session: false, - failureRedirect: "/dapp/login?redirect=/dapp/briefs/new", - }), - (req, res) => { - res.render("new-brief"); - } + "/briefs/new", + passport.authenticate("jwt", { + session: false, + failureRedirect: "/dapp/login?redirect=/dapp/briefs/new", + }), + (req, res) => { + res.render("new-brief"); + } ); router.get("/briefs/:id", (req, res) => { - res.render("brief-details"); + res.render("brief-details"); }); -router.get("/briefs/:id/apply", function (req, res, next) { +router.get( + "/briefs/:id/apply", + function (req, res, next) { passport.authenticate("jwt", { - session: false, - failureRedirect: `/dapp/login?redirect=/dapp/briefs/${req.params.id}/apply`, - })(req, res, next) -}, (req, res) => { + session: false, + failureRedirect: `/dapp/login?redirect=/dapp/briefs/${req.params.id}/apply`, + })(req, res, next); + }, + (req, res) => { res.render("submit-proposal"); -}); + } +); router.get("/briefs/:id/applications", (req, res) => { - res.render("brief-applications"); + res.render("brief-applications"); }); router.get("/briefs/:id/applications/:application_id", (req, res) => { - res.render("application-preview"); + res.render("application-preview"); }); router.get("/my-briefs", (req, res) => { - res.render("hirer-dashboard"); + res.render("hirer-dashboard"); }); router.get("/join", (req, res) => { - res.render("join"); + res.render("join"); }); router.get("/googlelogin", (req, res) => { - res.render("googlelogin"); + res.render("googlelogin"); }); router.get("/briefs/", (req, res) => { - res.render("briefs"); + res.render("briefs"); }); router.get("/freelancers/", (req, res) => { - res.render("freelancers"); + res.render("freelancers"); }); router.get( - "/freelancers/new", - passport.authenticate("jwt", { - session: false, - failureRedirect: "/dapp/login?redirect=/dapp/freelancers/new", - }), - (req, res) => { - res.render("new-freelancer"); - } + "/freelancers/new", + passport.authenticate("jwt", { + session: false, + failureRedirect: "/dapp/login?redirect=/dapp/freelancers/new", + }), + (req, res) => { + res.render("new-freelancer"); + } ); router.get("/freelancers/:username", (req, res) => { - res.render("freelancer-profile"); + res.render("freelancer-profile"); }); router.use((_req, res, next) => { - res.render("legacy"); + res.render("legacy"); }); -export default router; \ No newline at end of file +export default router; diff --git a/api/src/backend/tsconfig.json b/api/src/backend/tsconfig.json index ed9ac2c1..0032cc0a 100644 --- a/api/src/backend/tsconfig.json +++ b/api/src/backend/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ @@ -24,9 +24,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./" /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -47,7 +47,7 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "../../build", /* Specify an output folder for all emitted files. */ + "outDir": "../../build" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -69,13 +69,13 @@ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ diff --git a/api/src/frontend/components/accountChoice.tsx b/api/src/frontend/components/accountChoice.tsx index a9ad3a2e..d59b3725 100644 --- a/api/src/frontend/components/accountChoice.tsx +++ b/api/src/frontend/components/accountChoice.tsx @@ -1,36 +1,50 @@ -import { InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; +import { type InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; import { getWeb3Accounts } from "../utils/polkadot"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { Dialogue } from "./dialogue"; -type AccountChoiceProps = { - accountSelected: (account: InjectedAccountWithMeta) => void +interface AccountChoiceProps { + accountSelected: (account: InjectedAccountWithMeta) => void; } -export const AccountChoice = ({ accountSelected }: AccountChoiceProps): JSX.Element => { - const [accounts, setAccounts] = React.useState([]); +export const AccountChoice = ({ + accountSelected, +}: AccountChoiceProps): JSX.Element => { + const [accounts, setAccounts] = React.useState([]); - const fetchAndSetAccounts = async () => { - const _accounts = await getWeb3Accounts(); - setAccounts(_accounts); - }; + const fetchAndSetAccounts = async () => { + const _accounts = await getWeb3Accounts(); + setAccounts(_accounts); + }; - useEffect(() => { - void fetchAndSetAccounts(); - }, []); + useEffect(() => { + void fetchAndSetAccounts(); + }, []); - return - {accounts.map((account, index) => { - const { meta: { name, source } } = account; - return
  • - -
  • - })} - - } /> -} \ No newline at end of file + return ( + + {accounts.map((account, index) => { + const { + meta: { name, source }, + } = account; + return ( +
  • + +
  • + ); + })} + + } + /> + ); +}; diff --git a/api/src/frontend/components/alert.tsx b/api/src/frontend/components/alert.tsx index 33d08c63..66e78b42 100644 --- a/api/src/frontend/components/alert.tsx +++ b/api/src/frontend/components/alert.tsx @@ -1,34 +1,39 @@ -import React, { FunctionComponent } from 'react'; -import alertType from './alertType'; +import React from "react"; +import alertType from "./alertType"; -type AlertProps = { - type: alertType, - message: string +interface AlertProps { + type: alertType; + message: string; } const Alert = ({ type, message }: AlertProps): JSX.Element => { - let alertClassName = 'alert alert-dismissible fade show '; - let title = ''; + let alertClassName = "alert alert-dismissible fade show "; + let title = ""; - if (type === alertType.SUCCESS) { - alertClassName += 'alert-success'; - title = 'Success!'; - } else if (type === alertType.WARNING) { - alertClassName += 'alert-warning'; - title = 'Warning!'; - } else if (type === alertType.ERROR) { - alertClassName += 'alert-danger'; - title = 'Error!'; - } + if (type === alertType.SUCCESS) { + alertClassName += "alert-success"; + title = "Success!"; + } else if (type === alertType.WARNING) { + alertClassName += "alert-warning"; + title = "Warning!"; + } else if (type === alertType.ERROR) { + alertClassName += "alert-danger"; + title = "Error!"; + } - return ( -
    - {title} {message} - -
    - ); + return ( +
    + {title} {message} + +
    + ); }; -export default alert; \ No newline at end of file +export default alert; diff --git a/api/src/frontend/components/alertType.ts b/api/src/frontend/components/alertType.ts index 72098520..aa09f094 100644 --- a/api/src/frontend/components/alertType.ts +++ b/api/src/frontend/components/alertType.ts @@ -1,7 +1,7 @@ enum alertType { - SUCCESS = 'SUCCESS', - WARNING = 'WARNING', - ERROR = 'ERROR' + SUCCESS = "SUCCESS", + WARNING = "WARNING", + ERROR = "ERROR", } -export default alertType; \ No newline at end of file +export default alertType; diff --git a/api/src/frontend/components/approveMilestone.tsx b/api/src/frontend/components/approveMilestone.tsx index cb9f773d..b6dfca39 100644 --- a/api/src/frontend/components/approveMilestone.tsx +++ b/api/src/frontend/components/approveMilestone.tsx @@ -1,136 +1,186 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; import { CircularProgress } from "@rmwc/circular-progress"; import { Chip } from "@rmwc/chip"; -import '@rmwc/circular-progress/styles'; - -import * as polkadot from "../utils/polkadot"; -import { BasicTxResponse, ButtonState, Currency, Milestone, ProjectOnChain, ProjectState, User } from "../models"; -import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; -import { AccountChoice } from './accountChoice'; -import { ErrorDialog } from './errorDialog'; -import ChainService from '../services/chainService'; - -export type ApproveMilestoneProps = { - imbueApi: polkadot.ImbueApiInfo, - user: User, - projectOnChain: ProjectOnChain, - firstPendingMilestoneIndex: number - chainService: ChainService +import "@rmwc/circular-progress/styles"; + +import type * as polkadot from "../utils/polkadot"; +import { + type BasicTxResponse, + ButtonState, + Currency, + Milestone, + type ProjectOnChain, + ProjectState, + type User, +} from "../models"; +import type { InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; +import { AccountChoice } from "./accountChoice"; +import { ErrorDialog } from "./errorDialog"; +import type ChainService from "../services/chainService"; + +export interface ApproveMilestoneProps { + imbueApi: polkadot.ImbueApiInfo; + user: User; + projectOnChain: ProjectOnChain; + firstPendingMilestoneIndex: number; + chainService: ChainService; } -type ApproveMilestoneState = { - showPolkadotAccounts: boolean, - showErrorDialog: boolean, - errorMessage: String | null, - enableApproveButton: boolean, - displayVotingProgress: boolean, - percentageOfContributorsVoted: number, - status: string, - buttonState: ButtonState +interface ApproveMilestoneState { + showPolkadotAccounts: boolean; + showErrorDialog: boolean; + errorMessage: string | null; + enableApproveButton: boolean; + displayVotingProgress: boolean; + percentageOfContributorsVoted: number; + status: string; + buttonState: ButtonState; } -export const ApproveMilestone = ({ projectOnChain, imbueApi, firstPendingMilestoneIndex, chainService }: ApproveMilestoneProps): JSX.Element => { - const [polkadotAccountsVisible, showPolkadotAccounts] = useState(false); - const [errorDialogVisible, showErrorDialog] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); - const [status, setStatus] = useState("pendingApproval"); - const [percentageOfContributorsVoted, setPercentageOfContributorsVoted] = useState(-1); - const [approveButtonEnabled, enabledApproveButon] = useState(false); - const [displayVotingProgress, showVotingProgress] = useState(false); - const [buttonState, setButtonState] = useState(ButtonState.Default); - - const closeErrorDialog = () => { - showErrorDialog(false); +export const ApproveMilestone = ({ + projectOnChain, + imbueApi, + firstPendingMilestoneIndex, + chainService, +}: ApproveMilestoneProps): JSX.Element => { + const [polkadotAccountsVisible, showPolkadotAccounts] = useState(false); + const [errorDialogVisible, showErrorDialog] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [status, setStatus] = useState("pendingApproval"); + const [percentageOfContributorsVoted, setPercentageOfContributorsVoted] = + useState(-1); + const [approveButtonEnabled, enabledApproveButon] = useState(false); + const [displayVotingProgress, showVotingProgress] = useState(false); + const [buttonState, setButtonState] = useState(ButtonState.Default); + + const closeErrorDialog = () => { + showErrorDialog(false); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + showPolkadotAccounts(true); + }; + + const init = async () => { + await haveAllUsersVotedOnMilestone(); + }; + + useEffect(() => { + void init(); + }, []); + + const haveAllUsersVotedOnMilestone = async (): Promise => { + const totalContributions = projectOnChain.raisedFundsFormatted; + const milestoneVotes: any = ( + await imbueApi.imbue?.api.query.imbueProposals.milestoneVotes([ + projectOnChain.id, + firstPendingMilestoneIndex, + ]) + ).toHuman(); + if (!milestoneVotes) { + return false; } - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - showPolkadotAccounts(true); + const yayVotes = BigInt(milestoneVotes.yay.replaceAll(",", "")); + const nayVotes = BigInt(milestoneVotes.nay.replaceAll(",", "")); + const totalVotes = Number((yayVotes + nayVotes) / BigInt(1e12)); + const _percentageOfContributorsVoted = + (totalVotes / totalContributions) * 100; + const _approveButtonEnabled = totalVotes == totalContributions; + const _displayVotingProgress = + projectOnChain.projectState == ProjectState.OpenForVoting || + !approveButtonEnabled; + if (_percentageOfContributorsVoted != _percentageOfContributorsVoted) { + enabledApproveButon(_approveButtonEnabled); + setPercentageOfContributorsVoted(_percentageOfContributorsVoted); + showVotingProgress(_displayVotingProgress); } - - const init = async () => { - await haveAllUsersVotedOnMilestone(); - }; - - useEffect(() => { - void init(); - }, []); - - const haveAllUsersVotedOnMilestone = async (): Promise => { - const totalContributions = projectOnChain.raisedFundsFormatted; - const milestoneVotes: any = ((await imbueApi.imbue?.api.query.imbueProposals.milestoneVotes([projectOnChain.id, firstPendingMilestoneIndex]))).toHuman(); - if (!milestoneVotes) { - return false; - } - - const yayVotes = BigInt(milestoneVotes.yay.replaceAll(",", "")); - const nayVotes = BigInt(milestoneVotes.nay.replaceAll(",", "")); - const totalVotes = Number((yayVotes + nayVotes) / BigInt(1e12)); - const _percentageOfContributorsVoted = (totalVotes / totalContributions) * 100; - const _approveButtonEnabled = totalVotes == totalContributions; - const _displayVotingProgress = (projectOnChain.projectState == ProjectState.OpenForVoting || !approveButtonEnabled); - if (_percentageOfContributorsVoted != _percentageOfContributorsVoted) { - enabledApproveButon(_approveButtonEnabled); - setPercentageOfContributorsVoted(_percentageOfContributorsVoted); - showVotingProgress(_displayVotingProgress); + return totalVotes == totalContributions; + }; + + const approveMilestone = async ( + account: InjectedAccountWithMeta + ): Promise => { + showPolkadotAccounts(false); + setButtonState(ButtonState.Saving); + + const result: BasicTxResponse = await chainService.approveMilestone( + account, + projectOnChain, + firstPendingMilestoneIndex + ); + + // TODO timeout the while loop + while (true) { + if (result.status || result.txError) { + if (result.status) { + setButtonState(ButtonState.Done); + } else if (result.txError) { + setButtonState(ButtonState.Default); + showErrorDialog(true); + setErrorMessage(result.errorMessage); } - return totalVotes == totalContributions; + break; + } + await new Promise((f) => setTimeout(f, 1000)); } - - const approveMilestone = async (account: InjectedAccountWithMeta): Promise => { - showPolkadotAccounts(false); - setButtonState(ButtonState.Saving); - - const result: BasicTxResponse = await chainService.approveMilestone(account, - projectOnChain, - firstPendingMilestoneIndex); - - // TODO timeout the while loop - while (true) { - if (result.status || result.txError) { - if (result.status) { - setButtonState(ButtonState.Done); - } else if (result.txError) { - setButtonState(ButtonState.Default); - showErrorDialog(true); - setErrorMessage(result.errorMessage); - } - break; - } - await new Promise(f => setTimeout(f, 1000)); - } - } - - return projectOnChain && projectOnChain.milestones ? -
    - - - {polkadotAccountsVisible ? -

    - approveMilestone(account)} /> -

    - : null - } -
    - - - {displayVotingProgress ? -
    - } label={`${percentageOfContributorsVoted}% Contributors Voted`} /> -
    - : null - } - -
    -
    : <> -} \ No newline at end of file + }; + + return projectOnChain && projectOnChain.milestones ? ( +
    + + + {polkadotAccountsVisible ? ( +

    + { + await approveMilestone(account); + }} + /> +

    + ) : null} +
    + + + {displayVotingProgress ? ( +
    + } + label={`${percentageOfContributorsVoted}% Contributors Voted`} + /> +
    + ) : null} +
    +
    + ) : ( + <> + ); +}; diff --git a/api/src/frontend/components/briefFilter.tsx b/api/src/frontend/components/briefFilter.tsx index 0efa70b7..8a248004 100644 --- a/api/src/frontend/components/briefFilter.tsx +++ b/api/src/frontend/components/briefFilter.tsx @@ -1,36 +1,34 @@ import React from "react"; -import { BriefFilterOption, FilterOption } from "../types/briefs"; +import { type BriefFilterOption, type FilterOption } from "../types/briefs"; -type BriefFilterProps = { - label: string; - filter_options: Array; - filter_type: BriefFilterOption; -}; - -export const BriefFilter = ({ label, filter_options, filter_type }: BriefFilterProps): JSX.Element => { - return ( -
    -
    {label}
    -
    - {filter_options.map( - ({ value, interiorIndex }) => ( -
    - - -
    - ) - )} -
    -
    - ); +interface BriefFilterProps { + label: string; + filter_options: FilterOption[]; + filter_type: BriefFilterOption; } +export const BriefFilter = ({ + label, + filter_options, + filter_type, +}: BriefFilterProps): JSX.Element => { + return ( +
    +
    {label}
    +
    + {filter_options.map(({ value, interiorIndex }) => ( +
    + + +
    + ))} +
    +
    + ); +}; + export default BriefFilter; diff --git a/api/src/frontend/components/briefInsights.tsx b/api/src/frontend/components/briefInsights.tsx index bd20708f..7d5a499c 100644 --- a/api/src/frontend/components/briefInsights.tsx +++ b/api/src/frontend/components/briefInsights.tsx @@ -1,62 +1,62 @@ import React from "react"; import { FaDollarSign, FaRegCalendarAlt } from "react-icons/fa"; import { RiShieldUserLine } from "react-icons/ri"; -import { Brief } from "../models"; +import { type Brief } from "../models"; import "../../../public/submit-proposal.css"; // TODO: update css import { redirect } from "../utils"; import TimeAgo from "javascript-time-ago"; import en from "javascript-time-ago/locale/en"; interface BriefInsightsProps { - brief: Brief; + brief: Brief; } TimeAgo.addDefaultLocale(en); export const BriefInsights = ({ brief }: BriefInsightsProps) => { - const timeAgo = new TimeAgo("en-US"); - const timePosted = timeAgo.format(new Date(brief.created)); + const timeAgo = new TimeAgo("en-US"); + const timePosted = timeAgo.format(new Date(brief.created)); - const viewFullBrief = () => { - redirect(`briefs/${brief.id}/`); - }; + const viewFullBrief = () => { + redirect(`briefs/${brief.id}/`); + }; - return ( -
    -
    -
    -

    {brief.headline }

    -

    - View full brief -

    -
    -
    -

    {brief.description}

    -
    -
    Posted {timePosted}
    -
    -
    -
    - -
    -

    {brief.experience_level}

    -
    Experience Level
    -
    -
    -
    - -
    -

    ${Number(brief.budget).toLocaleString()}

    -
    Fixed Price
    -
    -
    -
    - -
    -

    {brief.duration}

    -
    Project length
    -
    -
    -
    + return ( +
    +
    +
    +

    {brief.headline}

    +

    + View full brief +

    - ); +
    +

    {brief.description}

    +
    +
    Posted {timePosted}
    +
    +
    +
    + +
    +

    {brief.experience_level}

    +
    Experience Level
    +
    +
    +
    + +
    +

    ${Number(brief.budget).toLocaleString()}

    +
    Fixed Price
    +
    +
    +
    + +
    +

    {brief.duration}

    +
    Project length
    +
    +
    +
    +
    + ); }; diff --git a/api/src/frontend/components/chat.tsx b/api/src/frontend/components/chat.tsx index 31f48bf1..c427d190 100644 --- a/api/src/frontend/components/chat.tsx +++ b/api/src/frontend/components/chat.tsx @@ -1,58 +1,63 @@ import React, { useEffect, useState } from "react"; import ReactDOMClient from "react-dom/client"; -import { Freelancer, User } from "../models"; -import { StreamChat } from 'stream-chat'; -import { Chat, Channel, ChannelHeader, MessageInput, MessageList, Thread, Window } from 'stream-chat-react'; -import 'stream-chat-react/dist/css/v2/index.css'; +import { Freelancer, type User } from "../models"; +import { type StreamChat } from "stream-chat"; +import { + Chat, + Channel, + ChannelHeader, + MessageInput, + MessageList, + Thread, + Window, +} from "stream-chat-react"; +import "stream-chat-react/dist/css/v2/index.css"; import { getStreamChat } from "../utils"; -export type ChatProps = { - user: User; - targetUser: User; -}; +export interface ChatProps { + user: User; + targetUser: User; +} export const ChatBox = ({ user, targetUser }: ChatProps) => { - const [client, setClient] = useState(); - - useEffect(() => { - async function setup() { - setClient(await getStreamChat()); - } - setup(); - }, []) - - if (client) { - const currentChannel = `${user.display_name} (${user.username}) <> ${targetUser.display_name} ${targetUser.username}`; - - client.connectUser( - { - id: user.username, - name: user.display_name, - }, - user.getstream_token, - ); - - const channel = client.channel('messaging', { - image: 'https://www.drupal.org/files/project-images/react.png', - name: currentChannel, - members: [user.username, targetUser.username], - }); - - return ( - - - - - - - - - - ); + const [client, setClient] = useState(); + + useEffect(() => { + async function setup() { + setClient(await getStreamChat()); } + setup(); + }, []); + + if (client != null) { + const currentChannel = `${user.display_name} (${user.username}) <> ${targetUser.display_name} ${targetUser.username}`; + + client.connectUser( + { + id: user.username, + name: user.display_name, + }, + user.getstream_token + ); + + const channel = client.channel("messaging", { + image: "https://www.drupal.org/files/project-images/react.png", + name: currentChannel, + members: [user.username, targetUser.username], + }); return ( -

    REACT_APP_GETSTREAM_API_KEY not found

    + + + + + + + + + ); -}; + } + return

    REACT_APP_GETSTREAM_API_KEY not found

    ; +}; diff --git a/api/src/frontend/components/contribute.tsx b/api/src/frontend/components/contribute.tsx index c1c93f74..f79605a4 100644 --- a/api/src/frontend/components/contribute.tsx +++ b/api/src/frontend/components/contribute.tsx @@ -1,108 +1,149 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState } from "react"; import { TextField } from "@rmwc/textfield"; -import '@rmwc/textfield/styles'; +import "@rmwc/textfield/styles"; -import * as polkadot from "../utils/polkadot"; -import { BasicTxResponse, Currency, Milestone, ProjectOnChain, ProjectState, User } from "../models"; -import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; -import { AccountChoice } from './accountChoice'; -import { ErrorDialog } from './errorDialog'; -import ChainService from '../services/chainService'; +import type * as polkadot from "../utils/polkadot"; +import { + type BasicTxResponse, + type Currency, + Milestone, + type ProjectOnChain, + ProjectState, + type User, +} from "../models"; +import type { InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; +import { AccountChoice } from "./accountChoice"; +import { ErrorDialog } from "./errorDialog"; +import type ChainService from "../services/chainService"; -export type ContributeProps = { - imbueApi: polkadot.ImbueApiInfo, - user: User, - projectOnChain: ProjectOnChain, - chainService: ChainService +export interface ContributeProps { + imbueApi: polkadot.ImbueApiInfo; + user: User; + projectOnChain: ProjectOnChain; + chainService: ChainService; } enum ButtonState { - Default, - Saving, - Done + Default, + Saving, + Done, } -type ContributeState = { - showPolkadotAccounts: boolean, - showErrorDialog: boolean, - errorMessage: String | null, - contribution: number, - status: string, - buttonState: ButtonState +interface ContributeState { + showPolkadotAccounts: boolean; + showErrorDialog: boolean; + errorMessage: string | null; + contribution: number; + status: string; + buttonState: ButtonState; } -export const Contribute = ({ chainService, projectOnChain }: ContributeProps): JSX.Element => { - const [polkadotAccountsVisible, showPolkadotAccounts] = useState(false); - const [errorDialogVisible, showErrorDialog] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); - const [contribution, setContribution] = useState(0); - const [status, setStatus] = useState("pendingApproval"); - const [buttonState, setButtonState] = useState(ButtonState.Default); +export const Contribute = ({ + chainService, + projectOnChain, +}: ContributeProps): JSX.Element => { + const [polkadotAccountsVisible, showPolkadotAccounts] = useState(false); + const [errorDialogVisible, showErrorDialog] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [contribution, setContribution] = useState(0); + const [status, setStatus] = useState("pendingApproval"); + const [buttonState, setButtonState] = useState(ButtonState.Default); - const contribute = async (account: InjectedAccountWithMeta): Promise => { - showPolkadotAccounts(false); - setButtonState(ButtonState.Saving); - const result: BasicTxResponse = await chainService.contribute(account, - projectOnChain, - BigInt(contribution * 1e12)); + const contribute = async ( + account: InjectedAccountWithMeta + ): Promise => { + showPolkadotAccounts(false); + setButtonState(ButtonState.Saving); + const result: BasicTxResponse = await chainService.contribute( + account, + projectOnChain, + BigInt(contribution * 1e12) + ); - // TODO timeout the while loop - while (true) { - if (result.status || result.txError) { - if (result.status) { - setButtonState(ButtonState.Done); - } else if (result.txError) { - setButtonState(ButtonState.Default); - showErrorDialog(true); - setErrorMessage(result.errorMessage); - } - break; - } - await new Promise(f => setTimeout(f, 1000)); + // TODO timeout the while loop + while (true) { + if (result.status || result.txError) { + if (result.status) { + setButtonState(ButtonState.Done); + } else if (result.txError) { + setButtonState(ButtonState.Default); + showErrorDialog(true); + setErrorMessage(result.errorMessage); } + break; + } + await new Promise((f) => setTimeout(f, 1000)); } + }; - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - showPolkadotAccounts(true); - } + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + showPolkadotAccounts(true); + }; - const closeErrorDialog = () => { - showErrorDialog(false); - } + const closeErrorDialog = () => { + showErrorDialog(false); + }; - return ( - projectOnChain.milestones ? -
    - - - {polkadotAccountsVisible ? -

    - contribute(account)} /> -

    - : null - } -
    - setContribution(parseFloat((event.target as HTMLInputElement).value))} - outlined - className="mdc-text-field" - prefix={`$${projectOnChain.currencyId as Currency}`} - label="Contribution Amount..." required /> - - -
    : <> - ); + return projectOnChain.milestones ? ( +
    + -} \ No newline at end of file + {polkadotAccountsVisible ? ( +

    + { + await contribute(account); + }} + /> +

    + ) : null} +
    + { + setContribution( + parseFloat((event.target as HTMLInputElement).value) + ); + }} + outlined + className="mdc-text-field" + prefix={`$${projectOnChain.currencyId}`} + label="Contribution Amount..." + required + /> + + +
    + ) : ( + <> + ); +}; diff --git a/api/src/frontend/components/dashboard-chat.tsx b/api/src/frontend/components/dashboard-chat.tsx index 48890798..b9a33f8c 100644 --- a/api/src/frontend/components/dashboard-chat.tsx +++ b/api/src/frontend/components/dashboard-chat.tsx @@ -1,51 +1,59 @@ import React, { useEffect, useState } from "react"; import ReactDOMClient from "react-dom/client"; -import { User } from "../models"; +import { type User } from "../models"; -import { StreamChat } from 'stream-chat'; -import { Chat, Channel, ChannelList, ChannelHeader, MessageInput, MessageList, Thread, Window } from 'stream-chat-react'; -import 'stream-chat-react/dist/css/v2/index.css'; +import { type StreamChat } from "stream-chat"; +import { + Chat, + Channel, + ChannelList, + ChannelHeader, + MessageInput, + MessageList, + Thread, + Window, +} from "stream-chat-react"; +import "stream-chat-react/dist/css/v2/index.css"; import { getStreamChat } from "../utils"; -export type DashboardProps = { - user: User; -}; +export interface DashboardProps { + user: User; +} export const DashboardChat = ({ user }: DashboardProps): JSX.Element => { - const [client, setClient] = useState(); - const filters = { members: { $in: [user.username] } } - - useEffect(() => { - async function setup() { - setClient(await getStreamChat()); - } - setup(); - }, []); + const [client, setClient] = useState(); + const filters = { members: { $in: [user.username] } }; - if(client) { - client.connectUser( - { - id: user.username, - name: user.username, - }, - user.getstream_token, - ); + useEffect(() => { + async function setup() { + setClient(await getStreamChat()); } + setup(); + }, []); - return ( - client ? - - - - - - - - - - - - :

    REACT_APP_GETSTREAM_API_KEY not found

    + if (client != null) { + client.connectUser( + { + id: user.username, + name: user.username, + }, + user.getstream_token ); -}; + } + return client != null ? ( + + + + + + + + + + + + ) : ( +

    REACT_APP_GETSTREAM_API_KEY not found

    + ); +}; diff --git a/api/src/frontend/components/dialogue.tsx b/api/src/frontend/components/dialogue.tsx index a1bfd364..d95e5c01 100644 --- a/api/src/frontend/components/dialogue.tsx +++ b/api/src/frontend/components/dialogue.tsx @@ -1,28 +1,39 @@ -import * as React from 'react'; -import { ReactElement } from "react"; +import * as React from "react"; +import { type ReactElement } from "react"; -export type DialogueProps = { - title: String, - content?: ReactElement - actionList: ReactElement +export interface DialogueProps { + title: string; + content?: ReactElement; + actionList: ReactElement; } -export const Dialogue = ({ title, content, actionList }: DialogueProps): JSX.Element => { - return ( -
    -
    -
    -

    {title}

    -
    - {content} -
      - {actionList} -
    -
    -
    -
    -
    +export const Dialogue = ({ + title, + content, + actionList, +}: DialogueProps): JSX.Element => { + return ( +
    +
    +
    +

    + {title} +

    +
    + {content} +
      + {actionList} +
    +
    - ); -} \ No newline at end of file +
    +
    +
    + ); +}; diff --git a/api/src/frontend/components/errorDialog.tsx b/api/src/frontend/components/errorDialog.tsx index 2bd8f014..535a2fd6 100644 --- a/api/src/frontend/components/errorDialog.tsx +++ b/api/src/frontend/components/errorDialog.tsx @@ -1,26 +1,37 @@ -import * as React from 'react'; -import { Dialog, DialogActions, DialogButton, DialogContent, DialogTitle } from "@rmwc/dialog"; +import * as React from "react"; +import { + Dialog, + DialogActions, + DialogButton, + DialogContent, + DialogTitle, +} from "@rmwc/dialog"; -export type ErrorDialogProps = { - errorMessage: String | null, - showDialog: boolean, - closeDialog: () => void +export interface ErrorDialogProps { + errorMessage: string | null; + showDialog: boolean; + closeDialog: () => void; } -export const ErrorDialog = ({ errorMessage, showDialog, closeDialog }: ErrorDialogProps): JSX.Element => { - return ( - { - closeDialog(); - }}> - Error - {errorMessage} - - - Ok - - - - ); -} \ No newline at end of file +export const ErrorDialog = ({ + errorMessage, + showDialog, + closeDialog, +}: ErrorDialogProps): JSX.Element => { + return ( + { + closeDialog(); + }} + > + Error + {errorMessage} + + + Ok + + + + ); +}; diff --git a/api/src/frontend/components/freelancerFilter.tsx b/api/src/frontend/components/freelancerFilter.tsx index 630d4d8f..619762b1 100644 --- a/api/src/frontend/components/freelancerFilter.tsx +++ b/api/src/frontend/components/freelancerFilter.tsx @@ -1,36 +1,37 @@ import React from "react"; -import { FreelancerFilterOption, FilterOption } from "../types/freelancers"; +import { + type FreelancerFilterOption, + type FilterOption, +} from "../types/freelancers"; -type FreelancerFilterProps = { - label: string; - filter_options: Array; - filter_type: FreelancerFilterOption; -}; - -export const FreelancerFilter = ({ label, filter_options, filter_type }: FreelancerFilterProps): JSX.Element => { - return ( -
    -
    {label}
    -
    - {filter_options.map( - ({ value, interiorIndex }) => ( -
    - - -
    - ) - )} -
    -
    - ); +interface FreelancerFilterProps { + label: string; + filter_options: FilterOption[]; + filter_type: FreelancerFilterOption; } +export const FreelancerFilter = ({ + label, + filter_options, + filter_type, +}: FreelancerFilterProps): JSX.Element => { + return ( +
    +
    {label}
    +
    + {filter_options.map(({ value, interiorIndex }) => ( +
    + + +
    + ))} +
    +
    + ); +}; + export default FreelancerFilter; diff --git a/api/src/frontend/components/fundingInfo.tsx b/api/src/frontend/components/fundingInfo.tsx index 986e5e73..8aef8922 100644 --- a/api/src/frontend/components/fundingInfo.tsx +++ b/api/src/frontend/components/fundingInfo.tsx @@ -1,62 +1,96 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; import { ProgressBar, Step } from "react-step-progress-bar"; import { LinearProgress } from "@rmwc/linear-progress"; -import '@rmwc/linear-progress/styles'; +import "@rmwc/linear-progress/styles"; import "react-step-progress-bar/styles.css"; -import { Currency, ProjectOnChain, ProjectState, User } from "../models"; +import { + type Currency, + type ProjectOnChain, + ProjectState, + User, +} from "../models"; -export type FundingInfoProps = { - projectOnChain: ProjectOnChain, - lastApprovedMilestoneIndex: number, +export interface FundingInfoProps { + projectOnChain: ProjectOnChain; + lastApprovedMilestoneIndex: number; } -export const FundingInfo = ({ projectOnChain, lastApprovedMilestoneIndex }: FundingInfoProps): JSX.Element => { - const [percentageFunded, setPercentageFunded] = useState(); +export const FundingInfo = ({ + projectOnChain, + lastApprovedMilestoneIndex, +}: FundingInfoProps): JSX.Element => { + const [percentageFunded, setPercentageFunded] = useState(); - useEffect(() => { - if (projectOnChain.milestones && !percentageFunded) { - const funded = (projectOnChain.raisedFundsFormatted / projectOnChain.requiredFundsFormatted) * 100; - if (Number(funded.toFixed(1)) != percentageFunded) { - setPercentageFunded(Number(funded.toFixed(1))); - } - } - }, [projectOnChain, percentageFunded]); - - return projectOnChain.milestones ? -
    -
    -
    - - {percentageFunded}% Funded -
    - Funding Goal - {projectOnChain.requiredFundsFormatted.toLocaleString()} - ${projectOnChain.currencyId as Currency}{' '} -
    -
    - -
    - - {projectOnChain.milestones.map((milestone, index, arr) => { - return ( - ( -
    -
    - )} - /> - ); - })} -
    - {projectOnChain.milestones.length} Milestones {''} -
    + useEffect(() => { + if (projectOnChain.milestones && !percentageFunded) { + const funded = + (projectOnChain.raisedFundsFormatted / + projectOnChain.requiredFundsFormatted) * + 100; + if (Number(funded.toFixed(1)) != percentageFunded) { + setPercentageFunded(Number(funded.toFixed(1))); + } + } + }, [projectOnChain, percentageFunded]); -
    + return projectOnChain.milestones ? ( +
    +
    +
    + + {percentageFunded}% Funded +
    + Funding Goal + + {projectOnChain.requiredFundsFormatted.toLocaleString()} + + + ${projectOnChain.currencyId} + {" "} +
    +
    -
    : <> -} \ No newline at end of file +
    + + {projectOnChain.milestones.map((milestone, index, arr) => { + return ( + ( +
    + )} + /> + ); + })} +
    + {projectOnChain.milestones.length} Milestones {""} +
    +
    +
    + ) : ( + <> + ); +}; diff --git a/api/src/frontend/components/milestoneItem.tsx b/api/src/frontend/components/milestoneItem.tsx index a4e413c7..d20edb96 100644 --- a/api/src/frontend/components/milestoneItem.tsx +++ b/api/src/frontend/components/milestoneItem.tsx @@ -1,50 +1,103 @@ import React, { useState, useEffect } from "react"; import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai"; import { BsCircleFill } from "react-icons/bs"; -import { Currency, Milestone, ProjectOnChain, ProjectState } from "../models"; +import { + type Currency, + type Milestone, + type ProjectOnChain, + ProjectState, +} from "../models"; -export type MilestonesItemProps = { - milestone: Milestone, - projectOnChain: ProjectOnChain, - toggleMilestone: (milestoneKey: number) => void, - isInVotingRound: boolean, - toggleActive: boolean +export interface MilestonesItemProps { + milestone: Milestone; + projectOnChain: ProjectOnChain; + toggleMilestone: (milestoneKey: number) => void; + isInVotingRound: boolean; + toggleActive: boolean; } -const MilestoneItem = ({ milestone, projectOnChain, toggleActive, toggleMilestone, isInVotingRound }: MilestonesItemProps): JSX.Element => { - +const MilestoneItem = ({ + milestone, + projectOnChain, + toggleActive, + toggleMilestone, + isInVotingRound, +}: MilestonesItemProps): JSX.Element => { const [formattedMilestoneValue, setFormattedMilestoneValue] = useState("0"); useEffect(() => { if (milestone) { - const milestoneValue = Number((milestone.percentage_to_unlock / 100) * projectOnChain.requiredFundsFormatted); + const milestoneValue = Number( + (milestone.percentage_to_unlock / 100) * + projectOnChain.requiredFundsFormatted + ); setFormattedMilestoneValue(milestoneValue.toLocaleString()); } }, []); - const displayMilestoneToggle = (): JSX.Element => { if (milestone.isApproved) { if (toggleActive) { - return
    Approved
    + return ( +
    + {" "} + {" "} + Approved {" "} + +
    + ); } else { - return
    Approved
    + return ( +
    + {" "} + {" "} + Approved {" "} + +
    + ); } } else if (isInVotingRound) { if (toggleActive) { - return
    Active
    + return ( +
    + {" "} + {" "} + Active {" "} + +
    + ); } else { - return
    Active
    + return ( +
    + {" "} + {" "} + Active {" "} + +
    + ); } - } else { if (toggleActive) { - return
    Not Started
    + return ( +
    + {" "} + + Not Started{" "} + {" "} + +
    + ); } else { - return
    Not Started
    + return ( +
    + {" "} + Not Started + +
    + ); } } - } + }; return (
    @@ -61,18 +114,21 @@ const MilestoneItem = ({ milestone, projectOnChain, toggleActive, toggleMileston
    Percentage of funds released - {milestone.percentage_to_unlock}% + + {milestone.percentage_to_unlock}% +
    Funding Released - {formattedMilestoneValue} ${projectOnChain.currencyId as Currency} + + {formattedMilestoneValue} ${projectOnChain.currencyId} +
    -
    ); -} +}; export default MilestoneItem; diff --git a/api/src/frontend/components/milestones.tsx b/api/src/frontend/components/milestones.tsx index 4a02a14c..0c8828e6 100644 --- a/api/src/frontend/components/milestones.tsx +++ b/api/src/frontend/components/milestones.tsx @@ -1,30 +1,39 @@ -import React, { useState } from 'react'; -import { ProjectOnChain, ProjectState } from "../models"; -import MilestoneItem from './milestoneItem'; +import React, { useState } from "react"; +import { type ProjectOnChain, ProjectState } from "../models"; +import MilestoneItem from "./milestoneItem"; -export type MilestonesProps = { - projectOnChain: ProjectOnChain, - firstPendingMilestoneIndex: number +export interface MilestonesProps { + projectOnChain: ProjectOnChain; + firstPendingMilestoneIndex: number; } -export const Milestones = ({ projectOnChain, firstPendingMilestoneIndex }: MilestonesProps): JSX.Element => { - const [activeMilestone, setActiveMilestone] = useState(0); +export const Milestones = ({ + projectOnChain, + firstPendingMilestoneIndex, +}: MilestonesProps): JSX.Element => { + const [activeMilestone, setActiveMilestone] = useState(0); - const toggleMilestone = (milestoneKey: number) => { - setActiveMilestone(milestoneKey != activeMilestone ? milestoneKey : -1); - }; + const toggleMilestone = (milestoneKey: number) => { + setActiveMilestone(milestoneKey != activeMilestone ? milestoneKey : -1); + }; - return projectOnChain.milestones ? -
    - {projectOnChain.milestones.map((milestone, index) => ( - - ))} -
    : <>; -} \ No newline at end of file + return projectOnChain.milestones ? ( +
    + {projectOnChain.milestones.map((milestone, index) => ( + + ))} +
    + ) : ( + <> + ); +}; diff --git a/api/src/frontend/components/option.tsx b/api/src/frontend/components/option.tsx index 4ed94683..a7a02e76 100644 --- a/api/src/frontend/components/option.tsx +++ b/api/src/frontend/components/option.tsx @@ -1,14 +1,20 @@ import React from "react"; -export type OptionProps = { +export interface OptionProps { label: string; value: string | number; checked?: boolean; children?: React.ReactNode; onSelect: () => void; -}; +} -export const Option = ({ label, value, checked, children, onSelect }: OptionProps): JSX.Element => { +export const Option = ({ + label, + value, + checked, + children, + onSelect, +}: OptionProps): JSX.Element => { return (
    @@ -25,4 +31,4 @@ export const Option = ({ label, value, checked, children, onSelect }: OptionProp
    {children}
    ); -} +}; diff --git a/api/src/frontend/components/progressBar.tsx b/api/src/frontend/components/progressBar.tsx index 05cb6071..81f59939 100644 --- a/api/src/frontend/components/progressBar.tsx +++ b/api/src/frontend/components/progressBar.tsx @@ -1,26 +1,31 @@ import React from "react"; -export type ProgressBarProps = { - titleArray: Array; +export interface ProgressBarProps { + titleArray: string[]; currentValue: number; -}; +} -export const ProgressBar = ({ titleArray, currentValue }: ProgressBarProps): JSX.Element => { +export const ProgressBar = ({ + titleArray, + currentValue, +}: ProgressBarProps): JSX.Element => { return (
    {titleArray?.map((title, index) => (
    = index ? "active" : "disabled" - }`} + className={`progress-step-circle ${ + currentValue >= index ? "active" : "disabled" + }`} >

    0 && index < titleArray.length - 1 - ? "center" - : index === titleArray.length - 1 + className={`progress-step-text ${ + index > 0 && index < titleArray.length - 1 + ? "center" + : index === titleArray.length - 1 ? "right" : "" - }`} + }`} > {title}

    @@ -30,10 +35,11 @@ export const ProgressBar = ({ titleArray, currentValue }: ProgressBarProps): JSX
    ); -} +}; diff --git a/api/src/frontend/components/submitMilestone.tsx b/api/src/frontend/components/submitMilestone.tsx index 2a3450b7..21d089fd 100644 --- a/api/src/frontend/components/submitMilestone.tsx +++ b/api/src/frontend/components/submitMilestone.tsx @@ -1,109 +1,159 @@ -import * as React from 'react'; +import * as React from "react"; import { Select } from "@rmwc/select"; -import '@rmwc/select/styles'; +import "@rmwc/select/styles"; -import * as polkadot from "../utils/polkadot"; -import { BasicTxResponse, ButtonState, Currency, Milestone, ProjectOnChain, ProjectState, User } from "../models"; -import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; -import { AccountChoice } from './accountChoice'; -import { ErrorDialog } from './errorDialog'; -import ChainService from '../services/chainService'; +import type * as polkadot from "../utils/polkadot"; +import { + type BasicTxResponse, + ButtonState, + Currency, + Milestone, + type ProjectOnChain, + ProjectState, + type User, +} from "../models"; +import type { InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; +import { AccountChoice } from "./accountChoice"; +import { ErrorDialog } from "./errorDialog"; +import type ChainService from "../services/chainService"; -export type SubmitMilestoneProps = { - imbueApi: polkadot.ImbueApiInfo, - user: User, - projectOnChain: ProjectOnChain, - chainService: ChainService +export interface SubmitMilestoneProps { + imbueApi: polkadot.ImbueApiInfo; + user: User; + projectOnChain: ProjectOnChain; + chainService: ChainService; } -type SubmitMilestoneState = { - showPolkadotAccounts: boolean, - showErrorDialog: boolean, - errorMessage: String | null, - milestoneKey: number, - status: string, - buttonState: ButtonState +interface SubmitMilestoneState { + showPolkadotAccounts: boolean; + showErrorDialog: boolean; + errorMessage: string | null; + milestoneKey: number; + status: string; + buttonState: ButtonState; } export class SubmitMilestone extends React.Component { - state: SubmitMilestoneState = { - showPolkadotAccounts: false, - showErrorDialog: false, - errorMessage: null, - milestoneKey: 0, - status: "pendingApproval", - buttonState: ButtonState.Default - } + state: SubmitMilestoneState = { + showPolkadotAccounts: false, + showErrorDialog: false, + errorMessage: null, + milestoneKey: 0, + status: "pendingApproval", + buttonState: ButtonState.Default, + }; - updateMilestoneValue(milestoneKey: number) { - this.setState({ milestoneKey: milestoneKey }); - } + updateMilestoneValue(milestoneKey: number) { + this.setState({ milestoneKey }); + } - handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - this.setState({ showPolkadotAccounts: true }); - } + handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + this.setState({ showPolkadotAccounts: true }); + }; - closeErrorDialog = () => { - this.setState({ showErrorDialog: false }); - } + closeErrorDialog = () => { + this.setState({ showErrorDialog: false }); + }; - async submitMilestone(account: InjectedAccountWithMeta): Promise { - await this.setState({ showPolkadotAccounts: false, buttonState: ButtonState.Saving }); - const result: BasicTxResponse = await this.props.chainService.submitMilestone(account, - this.props.projectOnChain, - this.state.milestoneKey); + async submitMilestone(account: InjectedAccountWithMeta): Promise { + await this.setState({ + showPolkadotAccounts: false, + buttonState: ButtonState.Saving, + }); + const result: BasicTxResponse = + await this.props.chainService.submitMilestone( + account, + this.props.projectOnChain, + this.state.milestoneKey + ); - // TODO timeout the while loop - while (true) { - if (result.status || result.txError) { - if (result.status) { - await this.setState({ buttonState: ButtonState.Done }); - } else if (result.txError) { - await this.setState({ buttonState: ButtonState.Default, showErrorDialog: true, errorMessage: result.errorMessage }); - } - break; - } - await new Promise(f => setTimeout(f, 1000)); + // TODO timeout the while loop + while (true) { + if (result.status || result.txError) { + if (result.status) { + await this.setState({ buttonState: ButtonState.Done }); + } else if (result.txError) { + await this.setState({ + buttonState: ButtonState.Default, + showErrorDialog: true, + errorMessage: result.errorMessage, + }); } + break; + } + await new Promise((f) => setTimeout(f, 1000)); } + } - render() { - if (this.props.projectOnChain.milestones) { - return ( -
    - + render() { + if (this.props.projectOnChain.milestones) { + return ( +
    + - {this.state.showPolkadotAccounts ? -

    - this.submitMilestone(account)} /> -

    - : null - } -
    - - -
    -
    - ); - } + {this.state.showPolkadotAccounts ? ( +

    + { + await this.submitMilestone(account); + }} + /> +

    + ) : null} +
    + + +
    +
    + ); } -} \ No newline at end of file + } +} diff --git a/api/src/frontend/components/tagsInput.tsx b/api/src/frontend/components/tagsInput.tsx index 165ce383..f7a493c3 100644 --- a/api/src/frontend/components/tagsInput.tsx +++ b/api/src/frontend/components/tagsInput.tsx @@ -1,12 +1,16 @@ -import React, { useState, KeyboardEvent } from "react"; +import React, { useState, type KeyboardEvent } from "react"; -export type TagsInputProps = { +export interface TagsInputProps { tags: string[]; suggestData: string[]; onChange: (tags: string[]) => void; -}; +} -export const TagsInput = ({ tags, suggestData, onChange }: TagsInputProps): JSX.Element => { +export const TagsInput = ({ + tags, + suggestData, + onChange, +}: TagsInputProps): JSX.Element => { const [vtags, setTags] = useState(tags); const [input, setInput] = useState(""); @@ -42,7 +46,9 @@ export const TagsInput = ({ tags, suggestData, onChange }: TagsInputProps): JSX. {tag}
    handleDelete(i)} + onClick={() => { + handleDelete(i); + }} > x
    @@ -53,19 +59,23 @@ export const TagsInput = ({ tags, suggestData, onChange }: TagsInputProps): JSX. className="new-tag-input" data-testid="tag-input" value={input} - onChange={(e) => setInput(e.target.value)} + onChange={(e) => { + setInput(e.target.value); + }} onKeyDown={handleKeyDown} />
    {suggestData - .filter((item: string) => vtags.indexOf(item) === -1) + .filter((item: string) => !vtags.includes(item)) .map((item, index) => (
    {item} addItem(item)} + onClick={() => { + addItem(item); + }} > + @@ -74,4 +84,4 @@ export const TagsInput = ({ tags, suggestData, onChange }: TagsInputProps): JSX.
    ); -} +}; diff --git a/api/src/frontend/components/textInput.tsx b/api/src/frontend/components/textInput.tsx index 29964a3f..4b39869d 100644 --- a/api/src/frontend/components/textInput.tsx +++ b/api/src/frontend/components/textInput.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; export interface TextInputProps - extends React.DetailedHTMLProps< - React.TextareaHTMLAttributes, - HTMLTextAreaElement - > { - maxLength?: number; - onChange: (e: React.ChangeEvent) => void; - title?: string; + extends React.DetailedHTMLProps< + React.TextareaHTMLAttributes, + HTMLTextAreaElement + > { + maxLength?: number; + onChange: (e: React.ChangeEvent) => void; + title?: string; } export const TextInput = (props: TextInputProps): JSX.Element => { @@ -19,17 +19,19 @@ export const TextInput = (props: TextInputProps): JSX.Element => { const getTitle = (title?: string): any => { if (title) { - return {title} + return {title}; } - } + }; return ( <> {getTitle(props.title)}