Gig Marketplace

Photo by RetroSupply on Unsplash

Gig Marketplace

Important Links and demo credentials

Live Site: https://prod-gig-marketplace.fly.dev/

Github Repo: https://github.com/nivekithan/gig-marketplace

Demo Credentials:
Email:
Password: password

Pangea service used

Following pangea services are used

  1. IP Intel: To check the reputation of IP and block off the requests that might be coming from bad actors

  2. URL Intel: To make sure that all the URLs provided by users in Gig description and proposals is not malicious

  3. User Intel: To make sure that the users are signing up to our service with breached password (Note: For some reason, I am getting almost all checks to be positive even if the password is generated by a password manager. I am pretty sure i am doing something wrong, once I figure out what that is I will enable it on the site)

  4. Embargo: To showcase this ability I have created a custom embargo list in the dashboard which bans North Korea. Therefore if you are from North Korea you will not be able to use this app

  5. Secure Audit Log: To store logs about users payment for buying credits and users withdrawing credits as money

Why I created Gig Marketplace

Gig marketplace is an application that allows anyone to create a gig for someone else to solve that for a price. Even though it is similar to lot of existing freelance platforms like Upwork, fiver. The reason I have decided to create it is to use openai embedding for searching and finding related gigs and learning how to use Pangea to secure (at least try to secure) an app that stores payment information.

You can checkout the application by using the following login credentials or you can sign up with your own account.

Demo Credentials:
Email:
Password: password

Live Site: https://prod-gig-marketplace.fly.dev/

Challenges I have faced

Calling pangea multiple times for same ip address

Calling an external API is always slow and gig marketplace calls Pangea minimum two times

  1. For Embargo service

  2. Ip Intel Service

for each request. This slows down app too much.

I have solved this problem by using combination of lru-cache and cachified package to cache the results from the pangea in lru-cache . Therefore only one request will hit the pangea and all rest of the requests will use the value from the cache.

You can reference this code for implementation


async function isReputedIpAddress(ipAddress: string) {
  return cachified({
    key: `ipaddress-reputation-${ipAddress}`,
    cache: lruCache,
    async getFreshValue() {
      const ipIntel = new IPIntelService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
      const res = await ipIntel.reputation(ipAddress);
      const score = res.result.data.score;

      return score < 90; // I could not figure out what is optimal value here
    },
  });
}

Getting certain value from database are slow

In certain routes of my application, I have code that does n + 1 requests based on condition. This causes huge delays while loading that specific route.

To solve it, I instead of sending all data at intial request. I send only the minimal content required to render the page and then I stream other data. This is achieved using Streaming data in remix

Here is a code sample showcasing this solution

export async function loader({ params, request }: LoaderFunctionArgs) {
  const { id } = RouteParamsSchema.parse(params);
  const userId = await requireUser(request);

  const [gig, numberOfProposals] = await Promise.all([
    gigById({ id }),
    getNumberOfProposalForGig({ gigId: id }),
  ]);

  if (gig === null) {
    throw new Response("Not found", { status: 404 });
  }

  const similarGigs = getSimilarGigs(gig).then((gig) =>
    gig.map(whiteLabelGigs),
  ); // Notice how we are not awaiting this promise

   // .....

  return defer({
    gig: whiteLabelGigs(gig),
     // ... Other fields
    noOfProposal: numberOfProposals,
    similarGigs: similarGigs, // Notice how we are sending a promise over wire
  });
}

function SimilarGigs() {
  const { similarGigs } = useLoaderData<typeof loader>();
  return (
    <Suspense fallback={<SimilarGigsSkeleton />}> // we will show skeleton still the promise gets reolved
      <Await resolve={similarGigs}>
        {(gigs) => {
          if (gigs.length === 0) {
            return null;
          }

          return (
            <div className="px-4 flex flex-col gap-y-4 w-[420px]">
              <TextTitle>Related Gigs</TextTitle>

              {gigs.map((gig) => {
                return (
                  <div
                    key={gig.id}
                    className="max-w-[420px] border p-4 rounded-md transition-colors hover:border-primary"
                  >
                    <Link to={`/app/gig/g/${gig.id}`}>
                      <GigInfo {...gig} />
                    </Link>
                  </div>
                );
              })}
            </div>
          );
        }}
      </Await>
    </Suspense>
  );
}

My usage of Pangea

As I shared earlier I have used Pangea for the following capabilities

  1. IP Intel: To check the reputation of IP and block off the requests that might be coming from bad actors

  2. URL Intel: To make sure that all the URLs provided by users in Gig description and proposals is not malicious

  3. User Intel: To make sure that the users are signing up to our service with breached password (Note: For some reason, I am getting almost all checks to be positive even if the password is generated by a password manager. I am pretty sure i am doing something wrong, once I figure out what that is I will enable it on the site)

  4. Embargo: To showcase this ability I have created a custom embargo list in the dashboard which bans North Korea. Therefore if you are from North Korea you will not be able to use this app

  5. Secure Audit Log: To store logs about users payment for buying credits and users withdrawing credits as money

I am sharing the code implementing these features

IP Intel

async function isReputedIpAddress(ipAddress: string) {
  return cachified({
    key: `ipaddress-reputation-${ipAddress}`,
    cache: lruCache,
    async getFreshValue() {
      const ipIntel = new IPIntelService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
      const res = await ipIntel.reputation(ipAddress);
      const score = res.result.data.score;

      return score < 90;
    },
  });
}

Url Intel


export async function verifyUrlisGood(url: string) {
  return cachified({
    key: `url-reputation-${url}`,
    cache: lruCache,
    async getFreshValue() {
      const urlIntel = new URLIntelService(
        env.PANGEA_AUTHN_TOKEN,
        pangeaConfig,
      );
      const res = await urlIntel.reputation(url);

      const isConsideredHarmfull = res.result.data.score > 90;

      return !isConsideredHarmfull;
    },
  });
}

User Intel

export async function isPasswordBreached(password: string) {
  const hash = sha256().update(password).digest("hex");

  return isPasswordBreachedImpl(hash);
}

async function isPasswordBreachedImpl(hash: string) {
  return cachified({
    key: `password-breach-${hash}`,
    cache: lruCache,
    async getFreshValue() {
      const userIntel = new UserIntelService(
        env.PANGEA_AUTHN_TOKEN,
        pangeaConfig,
      );
      const firstFive = hash.substring(0, 5);

      const res = await userIntel.passwordBreached(
        Intel.HashType.SHA256,
        firstFive,
        {
          provider: "spycloud",
        },
      );
      const isBreached = res.result.data.found_in_breach;

      return isBreached;
    },
  });
}

Embargo

async function isFromEmbargoedCountryImpl(ipAdress: string) {
  return cachified({
    key: `embargo-${ipAdress}`,
    cache: lruCache,
    async getFreshValue() {
      const embargo = new EmbargoService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
      const res = await embargo.ipCheck(ipAdress);
      const sanctions = res.result.sanctions;

      return Boolean(sanctions.length);
    },
  });
}

Audit Log

export async function storeBuyingCredit({
  userId,
  newCredit,
  oldCredit,
}: {
  userId: string;
  oldCredit: number;
  newCredit: number;
}) {
  const auditLog = new AuditService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
  await auditLog.log({
    action: "buy_credit",
    actor: userId,
    target: "credits",
    old: oldCredit.toString(),
    new: newCredit.toString(),
    message: "User brought credits",
    timestamp: new Date().toISOString(),
  });
}

export async function storeWithdrawingCredit({
  newCredit,
  oldCredit,
  userId,
}: {
  userId: string;
  oldCredit: number;
  newCredit: number;
}) {
  const auditLog = new AuditService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
  await auditLog.log({
    action: "buy_credit",
    actor: userId,
    target: "credits",
    old: oldCredit.toString(),
    new: newCredit.toString(),
    message: "User withdrawed credits",
    timestamp: new Date().toISOString(),
  });
}

export async function storeRewardingCredit({
  newCredit,
  oldCredit,
  userId,
}: {
  userId: string;
  oldCredit: number;
  newCredit: number;
}) {
  const auditLog = new AuditService(env.PANGEA_AUTHN_TOKEN, pangeaConfig);
  await auditLog.log({
    action: "reward_credit",
    actor: userId,
    target: "credits",
    old: oldCredit.toString(),
    new: newCredit.toString(),
    message: "User was rewarded with credits",
    timestamp: new Date().toISOString(),
  });
}

Conclusion

I am grateful to Pangea and Hashnode for conducting this hackathon. Pangea is surely an exciting service that I will use in the future if the use case requires it.

Sharing same important links again

Live Site: https://prod-gig-marketplace.fly.dev/

Github Repo: https://github.com/nivekithan/gig-marketplace

Demo Credentials:
Email:
Password: password