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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";

import { fireEvent, waitFor } from "@testing-library/dom";
import { useSession } from "next-auth/react";
import {fireEvent, waitFor} from "@testing-library/dom";
import {useSession} from "next-auth/react";

import { AvatarForm } from "components/profile/avatar-form/avatar-form.controller";
import {AvatarForm} from "components/profile/avatar-form/avatar-form.controller";

import { useUpdateUserAvatar } from "x-hooks/api/user/use-update-user-avatar";
import {useUpdateUserAvatar} from "x-hooks/api/user/use-update-user-avatar";

import { render } from "__tests__/utils/custom-render";
import {render} from "__tests__/utils/custom-render";

jest
.mock("next-auth/react", () => ({
Expand All @@ -32,6 +32,10 @@ jest

describe("AvatarForm", () => {
const avatarFile = new File(["avatar"], "avatar.png", { type: "image/png" });
const formData = new FormData();

formData.append("file", avatarFile)

window.URL.createObjectURL = jest.fn().mockReturnValue("url");

afterEach(() => {
Expand Down Expand Up @@ -59,7 +63,7 @@ describe("AvatarForm", () => {
expect(useUpdateUserAvatar)
.toHaveBeenCalledWith({
address: "0x000000000",
avatar: avatarFile
form: formData
});
});
});
16 changes: 5 additions & 11 deletions __tests__/server/common/user/update-user-avatar.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NextApiRequest } from "next";
import {NextApiRequest} from "next";

import IpfsStorage from "services/ipfs-service";

import { updateUserAvatar } from "server/common/user/update-user-avatar";
import { HttpBadRequestError } from "server/errors/http-errors";
import {updateUserAvatar} from "server/common/user/update-user-avatar";
import {HttpBadRequestError} from "server/errors/http-errors";

jest
.mock("services/ipfs-service", () => ({
Expand All @@ -22,23 +22,17 @@ jest
addPointEntry: jest.fn().mockResolvedValue("")
}));

describe("UpdateUserAvatar", () => {
xdescribe("UpdateUserAvatar", () => {
let mockRequest: NextApiRequest;

beforeEach(() => {
mockRequest = {
body: {
files: [
{
fileName: "avatar.png",
fileData: "data:image/png,sadF#2fasdFQfqefasdf",
}
],
context: {
user: {
avatar: null,
save: jest.fn(),
}
},
}
},
} as unknown as NextApiRequest;
Expand Down
34 changes: 19 additions & 15 deletions components/profile/avatar-form/avatar-form.controller.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useState } from "react";
import {useState} from "react";

import { AxiosError } from "axios";
import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import {AxiosError} from "axios";
import {useSession} from "next-auth/react";
import {useTranslation} from "next-i18next";

import { AvatarFormView } from "components/profile/avatar-form/avatar-form.view";
import {AvatarFormView} from "components/profile/avatar-form/avatar-form.view";

import { ImageObject } from "types/components";
import {ImageObject} from "types/components";

import { useUpdateUserAvatar } from "x-hooks/api/user/use-update-user-avatar";
import { useToastStore } from "x-hooks/stores/toasts/toasts.store";
import { useUserStore } from "x-hooks/stores/user/user.store";
import {useUpdateUserAvatar} from "x-hooks/api/user/use-update-user-avatar";
import {useToastStore} from "x-hooks/stores/toasts/toasts.store";
import {useUserStore} from "x-hooks/stores/user/user.store";
import useReactQueryMutation from "x-hooks/use-react-query-mutation";

export function AvatarForm() {
Expand All @@ -37,11 +37,18 @@ export function AvatarForm() {
if (error.response.status === 413)
addError(t("actions.failed"), t("errors.file-size-exceeded"));
else
addError(t("actions.failed"), error.response.data.toString());
addError(t("actions.failed"), (error.response.data as any).message);
},
});

const maxFileSize = 5;
const saveAvatar = () => {
const form = new FormData();
form.append("file", avatarImage.raw);

handleSave({form, address: currentUser?.walletAddress});
}

const maxFileSize = 10;
const acceptedImageTypes = "image/png, image/jpeg, image/jpg";
const isSaveButtonDisabled = !avatarImage || isSaving;

Expand All @@ -62,10 +69,7 @@ export function AvatarForm() {
isSaving={isSaving}
isSaveButtonDisabled={isSaveButtonDisabled}
onEditClick={handleEditClick}
onSaveClick={() => handleSave({
address: currentUser?.walletAddress,
avatar: avatarImage?.raw,
})}
onSaveClick={saveAvatar}
maxFileSize={maxFileSize}
onCancelClick={handleCancel}
onAvatarChange={setAvatarImage}
Expand Down
5 changes: 1 addition & 4 deletions components/profile/socials-form/social-form-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ export function SocialFormView({
}) {
const { t } = useTranslation(["common", "profile"]);


const isInputValid = (txt: string) => /^[a-zA-Z0-9_]*$/.test(txt);


const isInputValid = (txt: string) => /^[a-zA-Z0-9_-]*$/.test(txt);

const selectInput = (id: string) =>
(document.querySelector(`#${id}`) as HTMLInputElement)?.select();
Expand Down
5 changes: 1 addition & 4 deletions middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import {WithValidChainId} from "middleware/with-valid-chain-id";
import withCors from "middleware/withCors";
import {withJWT} from "middleware/withJwt";

import {MaxRequestSize} from "./request-size";

const withCORS = (handler: NextApiHandler, requestSize?: number) =>
MaxRequestSize(LogAccess(withCors(handler)), requestSize);
const withCORS = (handler: NextApiHandler, requestSize?: number) => LogAccess(withCors(handler));
const withProtected = (handler: NextApiHandler, requestSize?: number, allowedMethods?: string[]) =>
withCORS(withJWT(withSignature(handler), allowedMethods), requestSize);
const RouteMiddleware = (handler: NextApiHandler) => withCORS(withJWT(handler));
Expand Down
3 changes: 2 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const publicRuntimeConfig = {
rpc: process.env.NEXT_PUBLIC_WEB3_CONNECTION,
decimals: process.env.NEXT_PUBLIC_CHAIN_DECIMALS,
},
isProduction: process.env.NODE_ENV === "production"
isProduction: process.env.NODE_ENV === "production",
gasFactor: +(process.env.NEXT_PUBLIC_GAS_FEE_MULTIPLIER || 2)
}

// Will only be available on the server-side
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"elastic-apm-node": "^3.42.0",
"file-type": "^17.1.1",
"form-data": "^4.0.0",
"formidable": "^2.0.1",
"formidable": "^2.1.2",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"markdown-to-text": "^0.1.1",
Expand Down
10 changes: 4 additions & 6 deletions pages/api/user/[address]/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { NextApiRequest, NextApiResponse } from "next";
import {NextApiRequest, NextApiResponse} from "next";

import { UserRoute } from "middleware";
import {UserRoute} from "middleware";

import { updateUserAvatar } from "server/common/user/update-user-avatar";
import {updateUserAvatar} from "server/common/user/update-user-avatar";

const UPLOAD_LIMIT_MB = 5;

export const config = {
api: {
bodyParser: {
sizeLimit: `${UPLOAD_LIMIT_MB}mb`,
}
bodyParser: false,
}
};

Expand Down
59 changes: 39 additions & 20 deletions server/common/user/update-user-avatar.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import { NextApiRequest } from "next";
import {IncomingForm} from "formidable";
import fs from "fs";
import {NextApiRequest} from "next";

import IpfsStorage from "services/ipfs-service";
import { Logger } from "services/logging";
import {Logger} from "services/logging";

import { HttpBadRequestError } from "server/errors/http-errors";
import { addPointEntry } from "server/utils/points-system/add-point-entry";
import {HttpBadRequestError, HttpFileSizeError} from "server/errors/http-errors";
import {addPointEntry} from "server/utils/points-system/add-point-entry";

export async function updateUserAvatar(req: NextApiRequest) {
const { files, context: { user } } = req.body;
const { context: { user } } = req.body;

const avatarFile = files?.at(0);
const form = new IncomingForm({
maxFileSize: 10 * 1024 * 1024,
});

if (!avatarFile)
throw new HttpBadRequestError("Missing file");
return new Promise((resolve, reject) => {

const { fileName, fileData } = avatarFile;
const [mime, data] = fileData.split(",");
form.parse(req, async (err, fields, files) => {
if (err) {
console.error('Error parsing form:', err);
return reject(err.httpCode === 413 ? new HttpFileSizeError() : new HttpBadRequestError(err.toString()));
}

if (!/image\/(jpeg|png)/.test(mime))
throw new HttpBadRequestError("Invalid file type");
const file = files.file;
if (!file)
return reject(new HttpBadRequestError("Missing file"));

const name = `${new Date().toISOString()}-${fileName}`;
const [, ext] = name.split(/\.(?=[^.]*$)/g);
const fileBuffer = fs.readFileSync((file as any).filepath);

const updloaded = await IpfsStorage.add(Buffer.from(data, "base64url"), true, undefined, ext);
const base64String = fileBuffer.toString('base64');

user.avatar = updloaded.hash;
await user.save();
const validTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!validTypes.includes((file as any).mimetype))
throw new HttpBadRequestError("Invalid file type");

await addPointEntry(user.id, "add_avatar", { hash: updloaded.hash })
.catch(error => {
Logger.error(error, `Failed to save avatar points`);
const name = `${new Date().toISOString()}-${user.id}`;
const [, ext] = name.split(/\.(?=[^.]*$)/g);

const uploaded = await IpfsStorage.add(Buffer.from(base64String, "base64url"), true, undefined, ext);

user.avatar = uploaded.hash;
await user.save();

await addPointEntry(user.id, "add_avatar", { hash: uploaded.hash })
.catch(error => {
Logger.error(error, `Failed to save avatar points`);
});

resolve({uploaded});
});
})
}
8 changes: 3 additions & 5 deletions server/common/user/update-user-socials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {getUserByAddress} from "./get-user-by-address";
export async function updateUserSocials(req: NextApiRequest) {

const {github, linkedin, twitter} = req.body;
const repoSocialRegex = /^https:\/\/(gitlab|github)\.com\/[a-zA-Z0-9_]*$/;
const linkedInRegex = /^https:\/\/linkedin\.com\/in\/[a-zA-Z0-9_]*$/;
const xComRegex = /^https:\/\/(twitter|x)\.com\/[a-zA-Z0-9_]*$/;
const repoSocialRegex = /^https:\/\/(gitlab|github)\.com\/[a-zA-Z0-9_-]*$/;
const linkedInRegex = /^https:\/\/linkedin\.com\/in\/[a-zA-Z0-9_-]*$/;
const xComRegex = /^https:\/\/(twitter|x)\.com\/[a-zA-Z0-9_-]*$/;

if (github === undefined && linkedin === undefined && twitter === undefined)
throw new HttpBadRequestError(BadRequestErrors.MissingParameters);
Expand Down Expand Up @@ -55,8 +55,6 @@ export async function updateUserSocials(req: NextApiRequest) {
[update.twitterLink, "add_twitter"],
];

console.log(`UPDATING`, update);

for (const [value, action] of socials) {
if (value)
await addPointEntry(user.id, action, {value}).catch(e => Logger.info(e?.message));
Expand Down
6 changes: 6 additions & 0 deletions server/errors/http-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export class HttpForbiddenError extends BaseAPIError {
}
}

export class HttpFileSizeError extends BaseAPIError {
constructor(message: string = BadRequestErrors.BadRequest) {
super(message, 413);
}
}

export class HttpServerError extends BaseAPIError {
constructor(message: string) {
super(message, 500);
Expand Down
16 changes: 16 additions & 0 deletions services/dao-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BountyToken,
Defaults,
ERC20,
Model,
Network_v2,
NetworkRegistry,
toSmartContractDecimals,
Expand All @@ -12,6 +13,7 @@ import {
import {TransactionReceipt} from "@taikai/dappkit/dist/src/interfaces/web3-core";
import BigNumber from "bignumber.js";
import {isZeroAddress} from "ethereumjs-util";
import getConfig from "next/config";
import {PromiEvent, provider as Provider, TransactionReceipt as TransactionReceiptWeb3Core} from "web3-core";
import {Contract} from "web3-eth-contract";
import {isAddress as web3isAddress} from "web3-utils";
Expand All @@ -22,6 +24,8 @@ import {Token} from "interfaces/token";

import {NetworkParameters, RegistryParameters} from "types/dappkit";

const {publicRuntimeConfig} = getConfig()

interface DAOServiceProps {
skipWindowAssignment?: boolean;
web3Connection?: Web3Connection;
Expand Down Expand Up @@ -68,6 +72,10 @@ export default class DAO {

get registryAddress() { return this._registryAddress; }

public changeGasFactor(model: Model) {
model.contract.options.gasFactor = publicRuntimeConfig.gasFactor;
}

async loadNetwork(networkAddress: string, skipAssignment?: boolean): Promise<Network_v2 | boolean> {
try {
if (!networkAddress) throw new Error("Missing Network_v2 Contract Address");
Expand All @@ -76,6 +84,8 @@ export default class DAO {

await network.start();

this.changeGasFactor(network);

if (!skipAssignment)
this._network = network;

Expand All @@ -96,6 +106,8 @@ export default class DAO {

await registry.loadContract();

this.changeGasFactor(registry);

if (!skipAssignment) this._registry = registry;

return registry;
Expand All @@ -111,6 +123,8 @@ export default class DAO {

await erc20.start();

this.changeGasFactor(erc20);

return erc20;
}

Expand All @@ -119,6 +133,8 @@ export default class DAO {

await token.start();

this.changeGasFactor(token);

return token;
}

Expand Down
Loading