Skip to content

Commit

Permalink
feat: add auth to client-side validator app
Browse files Browse the repository at this point in the history
  • Loading branch information
steinerkelvin committed Oct 11, 2024
1 parent 7d9b00b commit 75f3411
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 142 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"netuid",
"perc",
"polkadot",
"trpc",
"uid",
"uids"
],
Expand Down
2 changes: 1 addition & 1 deletion apps/commune-governance/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

const AUTH_ORIGIN_DEFAULT = "https://governance.communeai.org";
const AUTH_ORIGIN_DEFAULT = "governance.communeai.org";

export const env = createEnv({
shared: {
Expand Down
117 changes: 13 additions & 104 deletions apps/commune-governance/src/trpc/react.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
"use client";

import type { TRPCLink } from "@trpc/client";
import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createTRPCClient,
httpBatchLink,
loggerLink,
TRPCClientError,
} from "@trpc/client";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { observable } from "@trpc/server/observable";
import SuperJSON from "superjson";

import type { AppRouter } from "@commune-ts/api";
import { createAuthLink, makeAuthenticateUserFn } from "@commune-ts/api/client";
import { useCommune } from "@commune-ts/providers/use-commune";
import { createAuthReqData, signData } from "@commune-ts/utils";

import { env } from "~/env";

Expand Down Expand Up @@ -47,48 +39,20 @@ export function TRPCReactProvider({ children }: { children: React.ReactNode }) {

const { signHex } = useCommune();

const [isAuthenticating, setIsAuthenticating] = useState(false);
const getStoredAuthorization = () => localStorage.getItem("authorization");
const setStoredAuthorization = (authorization: string) =>
localStorage.setItem("authorization", authorization);

const authenticateUser = async () => {
if (isAuthenticating) {
console.log("Already authenticating, skipping.");
return;
}
setIsAuthenticating(true);

try {
const sessionData = createAuthReqData(env.NEXT_PUBLIC_AUTH_ORIGIN);
const signedData = await signData(signHex, sessionData);

const authClient = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: getBaseUrl() + "/api/trpc",
transformer: SuperJSON,
}),
],
});

const result = await authClient.auth.startSession.mutate(signedData);

if (result.token && result.authenticationType) {
const authorization = `${result.authenticationType} ${result.token}`;
localStorage.setItem("authorization", authorization);
console.log("Authentication successful");
} else {
throw new Error("Invalid authentication response");
}
} catch (error) {
console.error("Authentication error:", error);
throw error;
} finally {
setIsAuthenticating(false);
}
};
const authenticateUser = makeAuthenticateUserFn(
getBaseUrl(),
env.NEXT_PUBLIC_AUTH_ORIGIN,
setStoredAuthorization,
signHex,
);

const trpcClient = api.createClient({
links: [
createAuthLink(authenticateUser),
createAuthLink(authenticateUser, getStoredAuthorization),
loggerLink({
enabled: (op) =>
env.NODE_ENV === "development" ||
Expand Down Expand Up @@ -119,63 +83,8 @@ export function TRPCReactProvider({ children }: { children: React.ReactNode }) {
);
}

function createAuthLink(
authenticateUser: () => Promise<void>,
): TRPCLink<AppRouter> {
return () => {
return ({ op, next }) => {
return observable<unknown, Error | TRPCClientError<AppRouter>>(
(observer) => {
let retried = false;

const execute = () => {
const subscription = next(op).subscribe({
next: (result) => {
observer.next(result);
},
error: (err) => {
if (
!retried &&
err instanceof TRPCClientError &&
err.data?.code === "UNAUTHORIZED"
) {
retried = true;
authenticateUser()
.then(() => {
op.context.headers = {
...(op.context.headers ?? {}),
authorization:
localStorage.getItem("authorization") ?? "",
};
execute();
})
.catch((error) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
observer.error(error);
});
} else {
observer.error(err);
}
},
complete: () => {
observer.complete();
},
});

return () => {
subscription.unsubscribe();
};
};

return execute();
},
);
};
};
}

const getBaseUrl = () => {
if (typeof window !== "undefined") return window.location.origin;
// eslint-disable-next-line no-restricted-properties
return `http://localhost:${process.env.PORT ?? 3000}`;
};
};
6 changes: 3 additions & 3 deletions apps/commune-governance/src/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
ProposalStatus,
} from "@commune-ts/types";
import {
bigintDivision,
bigintDivision_WRONG,
formatToken,
paramNameToDisplayName,
} from "@commune-ts/utils";
Expand Down Expand Up @@ -138,7 +138,7 @@ export function calcProposalFavorablePercent(
if (totalStake === 0n) {
return null;
}
const ratio = bigintDivision(stakeFor, totalStake);
const ratio = bigintDivision_WRONG(stakeFor, totalStake);
const percentage = ratio * 100;
return percentage;
}
Expand Down Expand Up @@ -194,7 +194,7 @@ export function handleProposalQuorumPercent(
): JSX.Element {
function quorumPercent(stakeFor: bigint, stakeAgainst: bigint): JSX.Element {
const percentage =
bigintDivision(stakeFor + stakeAgainst, totalStake) * 100;
bigintDivision_WRONG(stakeFor + stakeAgainst, totalStake) * 100;
const percentDisplay = `${Number.isNaN(percentage) ? "—" : percentage.toFixed(1)}%`;
return <span className="text-yellow-600">{`(${percentDisplay})`}</span>;
}
Expand Down
4 changes: 4 additions & 0 deletions apps/commune-validator/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

const AUTH_ORIGIN_DEFAULT = "validator.communeai.org";

export const env = createEnv({
shared: {
NODE_ENV: z
Expand All @@ -20,6 +22,7 @@ export const env = createEnv({
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
NEXT_PUBLIC_AUTH_ORIGIN: z.string().default(AUTH_ORIGIN_DEFAULT), // Origin URI used in the statement signed by the user to authenticate
NEXT_PUBLIC_WS_PROVIDER_URL: z.string().url(),
NEXT_PUBLIC_CACHE_PROVIDER_URL: z.string().url(),
},
Expand All @@ -28,6 +31,7 @@ export const env = createEnv({
*/
experimental__runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_AUTH_ORIGIN: process.env.NEXT_PUBLIC_AUTH_ORIGIN,
NEXT_PUBLIC_WS_PROVIDER_URL: process.env.NEXT_PUBLIC_WS_PROVIDER_URL,
NEXT_PUBLIC_CACHE_PROVIDER_URL: process.env.NEXT_PUBLIC_CACHE_PROVIDER_URL,
},
Expand Down
74 changes: 45 additions & 29 deletions apps/commune-validator/src/trpc/react.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,83 @@
"use client";

import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";

import type { AppRouter } from "@commune-ts/api";
import { createAuthLink, makeAuthenticateUserFn } from "@commune-ts/api/client";
import { useCommune } from "@commune-ts/providers/use-commune";

import { env } from "~/env";

const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
},
});

let clientQueryClientSingleton: QueryClient | undefined = undefined;
let clientQueryClientSingleton: QueryClient | undefined;
const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client
return createQueryClient();
} else {
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient());
if (!clientQueryClientSingleton) {
clientQueryClientSingleton = createQueryClient();
}
return clientQueryClientSingleton;
}
};

export const api = createTRPCReact<AppRouter>();

export function TRPCReactProvider(props: { children: React.ReactNode }) {
export function TRPCReactProvider({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
transformer: SuperJSON,
url: getBaseUrl() + "/api/trpc",
headers() {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
const { signHex } = useCommune();

const getStoredAuthorization = () => localStorage.getItem("authorization");
const setStoredAuthorization = (authorization: string) =>
localStorage.setItem("authorization", authorization);

const authenticateUser = makeAuthenticateUserFn(
getBaseUrl(),
env.NEXT_PUBLIC_AUTH_ORIGIN,
setStoredAuthorization,
signHex,
);

const trpcClient = api.createClient({
links: [
createAuthLink(authenticateUser, getStoredAuthorization),
loggerLink({
enabled: (op) =>
env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
httpBatchLink({
url: getBaseUrl() + "/api/trpc",
headers() {
const headers: Record<string, string> = {};
headers["x-trpc-source"] = "nextjs-react";
const authorization = localStorage.getItem("authorization");
if (authorization) {
headers.authorization = authorization;
}
return headers;
},
transformer: SuperJSON,
}),
],
});

return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
{children}
</api.Provider>
</QueryClientProvider>
);
Expand Down
4 changes: 3 additions & 1 deletion apps/commune-worker/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ export interface WorkerProps {
}

// -- Constants -- //
export const NETUID_ZERO = 0;

export const CONSENSUS_NETUID = 0;
export const BLOCK_TIME = 8000;
export const DAO_EXPIRATION_TIME = 75600; // 7 days in blocks

// -- Functions -- //

export function log(...args: unknown[]) {
const [first, ...rest] = args;
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Expand Down
1 change: 0 additions & 1 deletion apps/commune-worker/src/weights.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { bigintDivision } from "@commune-ts/subspace/utils";
import { assert } from "tsafe";

/** Related to weights computation */

Expand Down
4 changes: 2 additions & 2 deletions apps/commune-worker/src/workers/module-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@commune-ts/subspace/queries";

import type { WorkerProps } from "../common";
import { BLOCK_TIME, isNewBlock, log, NETUID_ZERO, sleep } from "../common";
import { BLOCK_TIME, isNewBlock, log, CONSENSUS_NETUID, sleep } from "../common";
import { upsertModuleData } from "../db";
import { SubspaceModuleToDatabase } from "../db/type-transformations.js";

Expand All @@ -30,7 +30,7 @@ export async function moduleFetcherWorker(props: WorkerProps) {

const modules = await queryRegisteredModulesInfo(
lastBlock.apiAtBlock,
NETUID_ZERO,
CONSENSUS_NETUID,
props.lastBlock.blockNumber,
);
const modulesData = modules.map((module) =>
Expand Down
4 changes: 3 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
".": {
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
}
},
"./client": "./src/client.ts"
},
"scripts": {
"build": "tsc",
Expand All @@ -26,6 +27,7 @@
"@polkadot/util": "catalog:",
"@polkadot/util-crypto": "catalog:",
"@trpc/server": "catalog:",
"@trpc/client": "catalog:",
"jsonwebtoken": "^9.0.2",
"superjson": "2.2.1",
"zod": "catalog:"
Expand Down
Loading

0 comments on commit 75f3411

Please sign in to comment.