Skip to content
This repository has been archived by the owner on Apr 1, 2024. It is now read-only.

Gateway Template App Backend Notes #4

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nodejs 18.15.0
nodejs 20.11.1
181 changes: 70 additions & 111 deletions handlers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Handler } from "openapi-backend";
// TODO: Figure out a better way to handle the type checking of the OpenAPI
import type * as T from "../types/openapi.js";
import {
getApi,
Expand All @@ -8,12 +9,18 @@ import {
getProviderHttp,
getProviderKey,
} from "../services/frequency.js";
import { generateChallenge, createAuthToken, getMsaByPublicKey, useChallenge } from "../services/auth.js";
import {
generateChallenge,
createAuthToken,
getMsaByPublicKey,
useChallenge,
} from "../services/auth.js";
import { AnnouncementType } from "../services/dsnp.js";
import { getSchemaId } from "../services/announce.js";
import { getIpfsGateway } from "../services/ipfs.js";
import { signatureVerify } from "@polkadot/util-crypto";
import { hexToU8a, numberToU8a } from "@polkadot/util";
import { parseMessage, SiwsMessage } from "@talismn/siws";

// Environment Variables
const providerId = process.env.PROVIDER_ID;
Expand All @@ -34,31 +41,74 @@ const addProviderSchemas = [
// Make sure they are sorted.
addProviderSchemas.sort();

export const authChallenge: Handler<{}> = async (_c, _req, res) => {
const response: T.Paths.AuthChallenge.Responses.$200 = { challenge: generateChallenge() };
return res.status(200).json(response);
};
// TODO: Make work with `@frequency-chain/siwf`
export const authLogin2: Handler<T.Paths.AuthLogin2.RequestBody> = async (
c,
_req,
res,
) => {
const { signIn, signUp } = c.request.requestBody;
const api = await getApi();
if (signUp?.extrinsics) {
// TODO: (Might be part of the library)
// Verify each call's data, signatures, and expiration

export const authLogin: Handler<T.Paths.AuthLogin.RequestBody> = async (c, _req, res) => {
const { publicKey, encodedValue, challenge } = c.request.requestBody;
const msaId = await getMsaByPublicKey(publicKey);
if (!msaId || !useChallenge(challenge)) return res.status(401).send();
// Validate Challenge signature
const { isValid } = signatureVerify(challenge, encodedValue, publicKey);
if (!isValid) return res.status(401).send();
const transactions = signUp?.extrinsics.map((e) => e.encodedExtrinsic);
const txns = transactions?.map((t) => api.tx(t));
const calls = api.registry.createType("Vec<Call>", txns);

const response: T.Paths.AuthLogin.Responses.$200 = {
accessToken: await createAuthToken(c.request.requestBody.publicKey),
expires: Date.now() + 60 * 60 * 24,
dsnpId: msaId,
};
return res.status(200).json(response);
await api.tx.frequencyTxPayment
.payWithCapacityBatchAll(calls)
.signAndSend(
getProviderKey(),
{ nonce: await getNonce() },
({ status, dispatchError }) => {
if (dispatchError) {
console.error("ERROR in Signup: ", dispatchError.toHuman());
} else if (status.isInBlock || status.isFinalized) {
console.log("Account signup processed", status.toHuman());
}
},
);
}
// Is signin always required? Assume Yes for now for this code
if (signIn.siwsPayload) {
const parsedSignin = parseMessage(signIn.siwsPayload.message);
const publicKey = parsedSignin.address;
// TODO Verification like domain
if (false && parsedSignin.domain !== "") {
// return res.status(401).send();
}
// Verify Signature
const { isValid } = signatureVerify(
parsedSignin.prepareMessage(),
signIn.siwsPayload?.signature,
publicKey,
);
if (!isValid) return res.status(401).send();

// TODO: Burn the nonce for as long as expiration is to stop MitM attacks
// if (isNonceValid(parsedSignin.nonce, parsedSignin.expirationTime) {
// return res.status(401).send();
// }

const response: T.Paths.AuthLogin2.Responses.$200 = {
accessToken: await createAuthToken(publicKey),
expires: Date.now() + 60 * 60 * 24,
};
return res.status(200).json(response);
}

// We got some bad data if we got here.
return res.status(500).send();
};

export const authLogout: Handler<{}> = async (_c, _req, res) => {
return res.status(201);
};

// TODO: Figure out a better way to do this perhaps?
// It provides to the frontend the various direct conenctions it might need
export const authProvider: Handler<{}> = async (_c, _req, res) => {
const response: T.Paths.AuthProvider.Responses.$200 = {
nodeUrl: getProviderHttp(),
Expand All @@ -70,73 +120,8 @@ export const authProvider: Handler<{}> = async (_c, _req, res) => {
return res.status(200).json(response);
};

export const authCreate: Handler<T.Paths.AuthCreate.RequestBody> = async (c, _req, res) => {
try {
const api = await getApi();
const publicKey = c.request.requestBody.publicKey;

const addProviderData = {
authorizedMsaId: providerId,
expiration: c.request.requestBody.expiration,
schemaIds: addProviderSchemas,
};

const createSponsoredAccountWithDelegation = api.tx.msa.createSponsoredAccountWithDelegation(
publicKey,
{ Sr25519: c.request.requestBody.addProviderSignature },
addProviderData
);

const handleBytes = api.registry.createType("Bytes", c.request.requestBody.baseHandle);
const handlePayload = {
baseHandle: handleBytes,
expiration: c.request.requestBody.expiration,
};

const claimHandle = api.tx.handles.claimHandle(
publicKey,
{ Sr25519: c.request.requestBody.handleSignature },
handlePayload
);

// Validate the expiration and the signature before submitting them
const blockNum = await getCurrentBlockNumber();
if (blockNum > handlePayload.expiration) return res.status(409).send();

const claimHandlePayload = api.registry.createType("CommonPrimitivesHandlesClaimHandlePayload", handlePayload);

const { isValid } = signatureVerify(
claimHandlePayload.toU8a(),
hexToU8a(c.request.requestBody.handleSignature),
publicKey
);
if (!isValid) return res.status(401).send();

const calls = [createSponsoredAccountWithDelegation, claimHandle];

// TEMP: Undo the actual submission for now.s
// Trigger it and just log if there is an error later
await api.tx.frequencyTxPayment
.payWithCapacityBatchAll(calls)
.signAndSend(getProviderKey(), { nonce: await getNonce() }, ({ status, dispatchError }) => {
if (dispatchError) {
console.error("ERROR: ", dispatchError.toHuman());
} else if (status.isInBlock || status.isFinalized) {
console.log("Account Created", status.toHuman());
}
});

const response: T.Paths.AuthCreate.Responses.$200 = {
accessToken: await createAuthToken(c.request.requestBody.publicKey),
expires: Date.now() + 60 * 60 * 24,
};
return res.status(200).json(response);
} catch (e) {
console.error(e);
return res.status(500).send();
}
};

// This allows the user to get their logged in MSA.
// TODO: Figure out how to handle the time between when a user signs up and user has an MSA
export const authAccount: Handler<{}> = async (c, _req, res) => {
try {
const msaId = c.security?.tokenAuth?.msaId;
Expand All @@ -160,29 +145,3 @@ export const authAccount: Handler<{}> = async (c, _req, res) => {
return res.status(500).send();
}
};

export const authDelegate: Handler<T.Paths.AuthDelegate.RequestBody> = async (c, _req, res) => {
const response: T.Paths.AuthDelegate.Responses.$200 = {
accessToken: await createAuthToken(c.request.requestBody.publicKey),
expires: Date.now() + 60 * 60 * 24,
};
return res.status(200).json(response);
};

export const authHandles: Handler<T.Paths.AuthHandles.RequestBody> = async (c, _req, res) => {
const response: T.Paths.AuthHandles.Responses.$200 = [];
const api = await getApi();
for await (const publicKey of c.request.requestBody) {
const msaId = await api.query.msa.publicKeyToMsaId(publicKey);
if (msaId.isSome) {
const handle = await api.rpc.handles.getHandleForMsa(msaId.value);
if (handle.isSome) {
response.push({
publicKey,
handle: `${handle.value.base_handle}.${handle.value.suffix}`,
});
}
}
}
return res.status(200).json(response);
};
103 changes: 67 additions & 36 deletions handlers/content.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// TODO: Figure out how to integrate with Content Watcher and Publishing Services

import { Context, Handler } from "openapi-backend";
import Busboy from "busboy";
import type * as T from "../types/openapi.js";
import { ipfsPin, ipfsUrl } from "../services/ipfs.js";
import * as dsnp from "../services/dsnp.js";
import { createImageAttachment, createImageLink, createNote } from "@dsnp/activity-content/factories";
import {
createImageAttachment,
createImageLink,
createNote,
} from "@dsnp/activity-content/factories";
import { publish } from "../services/announce.js";
import { getPostsInRange } from "../services/feed.js";
import { getCurrentBlockNumber } from "../services/frequency.js";
Expand All @@ -17,7 +23,11 @@ type File = {
info: Busboy.FileInfo;
};

export const getUserFeed: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetUserFeed.QueryParameters>, _req, res) => {
export const getUserFeed: Handler<{}> = async (
c: Context<{}, {}, T.Paths.GetUserFeed.QueryParameters>,
_req,
res,
) => {
/// T.Paths.GetFeed.PathParameters, T.Paths.GetFeed.QueryParameters,
const { newestBlockNumber, oldestBlockNumber } = c.request.query;
// Default to now
Expand All @@ -39,9 +49,15 @@ export const getUserFeed: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetUse
return res.status(200).json(response);
};

export const getFeed: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetFeed.QueryParameters>, _req, res) => {
export const getFeed: Handler<{}> = async (
c: Context<{}, {}, T.Paths.GetFeed.QueryParameters>,
_req,
res,
) => {
// Return only items from who the user follows
const msaId = c.security.tokenAuth.msaId || (await getMsaByPublicKey(c.security.tokenAuth.publicKey));
const msaId =
c.security.tokenAuth.msaId ||
(await getMsaByPublicKey(c.security.tokenAuth.publicKey));

if (typeof msaId !== "string") {
return res.status(404).send();
Expand All @@ -68,7 +84,11 @@ export const getFeed: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetFeed.Qu
}
};

export const getDiscover: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetDiscover.QueryParameters>, _req, res) => {
export const getDiscover: Handler<{}> = async (
c: Context<{}, {}, T.Paths.GetDiscover.QueryParameters>,
_req,
res,
) => {
const { newestBlockNumber, oldestBlockNumber } = c.request.query;
// Default to now
const newest = newestBlockNumber ?? (await getCurrentBlockNumber());
Expand All @@ -83,39 +103,45 @@ export const getDiscover: Handler<{}> = async (c: Context<{}, {}, T.Paths.GetDis
return res.status(200).json(response);
};

export const createBroadcast: Handler<T.Paths.CreateBroadcast.RequestBody> = async (c, req, res) => {
export const createBroadcast: Handler<
T.Paths.CreateBroadcast.RequestBody
> = async (c, req, res) => {
try {
const msaId = c.security.tokenAuth.msaId || (await getMsaByPublicKey(c.security.tokenAuth.publicKey));
const msaId =
c.security.tokenAuth.msaId ||
(await getMsaByPublicKey(c.security.tokenAuth.publicKey));
const bb = Busboy({ headers: req.headers });

const formAsync: Promise<[Fields, File[]]> = new Promise((resolve, reject) => {
const files: File[] = [];
const fields: Fields = {};
bb.on("file", (name, file, info) => {
// Take the file to a in memory buffer. This might be a bad idea.
const chunks: Buffer[] = [];
file
.on("data", (chunk) => {
chunks.push(chunk);
const formAsync: Promise<[Fields, File[]]> = new Promise(
(resolve, reject) => {
const files: File[] = [];
const fields: Fields = {};
bb.on("file", (name, file, info) => {
// Take the file to a in memory buffer. This might be a bad idea.
const chunks: Buffer[] = [];
file
.on("data", (chunk) => {
chunks.push(chunk);
})
.on("close", () => {
files.push({
name,
file: Buffer.concat(chunks),
info,
});
});
})
.on("field", (name, val, info) => {
fields[name] = val;
})
.on("error", (e) => {
reject(e);
})
.on("close", () => {
files.push({
name,
file: Buffer.concat(chunks),
info,
});
resolve([fields, files]);
});
})
.on("field", (name, val, info) => {
fields[name] = val;
})
.on("error", (e) => {
reject(e);
})
.on("close", () => {
resolve([fields, files]);
});
});
},
);
req.pipe(bb);
const [fields, files] = await formAsync;

Expand All @@ -124,13 +150,18 @@ export const createBroadcast: Handler<T.Paths.CreateBroadcast.RequestBody> = asy
.filter((x) => x.name === "images")
.map(async (image) => {
const { cid, hash } = await ipfsPin(image.info.mimeType, image.file);
return createImageAttachment([createImageLink(ipfsUrl(cid), image.info.mimeType, [hash])]);
})
return createImageAttachment([
createImageLink(ipfsUrl(cid), image.info.mimeType, [hash]),
]);
}),
);

const note = createNote(fields.content, new Date(), { attachment });
const noteString = JSON.stringify(note);
const { cid, hash: contentHash } = await ipfsPin("application/json", Buffer.from(noteString, "utf8"));
const { cid, hash: contentHash } = await ipfsPin(
"application/json",
Buffer.from(noteString, "utf8"),
);

const announcement = fields.inReplyTo
? dsnp.createReply(msaId!, ipfsUrl(cid), contentHash, fields.inReplyTo)
Expand Down Expand Up @@ -172,7 +203,7 @@ export const editContent: Handler<T.Paths.EditContent.RequestBody> = async (
// , T.Paths.EditContent.PathParameters
c,
_req,
res
res,
) => {
const response: T.Paths.EditContent.Responses.$200 = {
fromId: "123",
Expand Down
Loading