Important Links and demo credentials
Live Site: https://prod-gig-marketplace.fly.dev/
Github Repo: https://github.com/nivekithan/gig-marketplace
Demo Credentials:
Email: demo@example.com
Password: password
Pangea service used
Following pangea services are used
IP Intel: To check the reputation of IP and block off the requests that might be coming from bad actors
URL Intel: To make sure that all the URLs provided by users in Gig description and proposals is not malicious
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)
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
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: demo@example.com
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
For Embargo service
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
IP Intel: To check the reputation of IP and block off the requests that might be coming from bad actors
URL Intel: To make sure that all the URLs provided by users in Gig description and proposals is not malicious
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)
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
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: demo@example.com
Password: password