Skip to content

Commit d56defa

Browse files
andreip136ndrppgiurgiur99greptile-apps[bot]
authored
Expose node tokens (#489)
* add node tokens context * fix context hydrate * fix hydrate * add node tokens button in profile dropdown * generate token in summary * update validation * add friendly node name * add nodes tokens list component * add initial density prop to table * add node tokens list component * update styling * update oceanjs * Revert "update oceanjs" This reverts commit 7a8a137. * revoke auth token * update ocean.js * add validUntil when creating token * convert token validity to ms * update ocean.js * fix: lock uuid package to 8.1 * add output file tracing for failing esm module * update to node v24 * use standalone output * update next config * add output tracing async & generator * async generator function too * revert uuid lock * add create auth token * fix: pin uuid version to cjs support * Update src/components/run-job/generate-token-card.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * remove cast * remove cast * handle success false * fix address switch briefly writes Account A's tokens into Account B's localStorage key * Update src/context/node-tokens.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix expiry value validation * catch local storage save error * fix account address ref --------- Co-authored-by: ndrpp <popandrei230@yahoo.com> Co-authored-by: giurgiur99 <giurgiur99@gmail.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent cff04e7 commit d56defa

23 files changed

Lines changed: 1786 additions & 1082 deletions

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20.16.0
1+
v24.15.0

next.config.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ const nextConfig = {
77
'@walletconnect/ethereum-provider',
88
'@walletconnect/universal-provider',
99
],
10+
// Upstream bug: Next 16 vendors @vercel/nft 0.27.1, which predates support
11+
// for the `module-sync` exports condition (added in nft 0.30.0). These
12+
// get-intrinsic helpers point `module-sync` at a separate `require.mjs`;
13+
// nft traces only `index.js`, but Node >= 22.10 (we run 24) resolves the
14+
// CJS require to `require.mjs` at runtime. Vercel ships only traced files,
15+
// so the un-traced `require.mjs` is missing -> MODULE_NOT_FOUND.
16+
// Remove this once a Next release bundles nft >= 0.30.0.
17+
// See https://github.com/vercel/next.js/issues/90567
18+
outputFileTracingIncludes: {
19+
'/**/*': [
20+
'./node_modules/async-function/**/*',
21+
'./node_modules/async-generator-function/**/*',
22+
'./node_modules/generator-function/**/*',
23+
],
24+
},
1025
turbopack: {
1126
rules: {
1227
'*.svg': {

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"private": true,
77
"scripts": {
88
"dev": "next dev",
9-
"build": "NODE_ENV=production next build --webpack",
9+
"build": "NODE_OPTIONS=\"--max-old-space-size=4096\" NODE_ENV=production next build --webpack",
1010
"start": "next start",
1111
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx .",
1212
"lint:fix": "eslint --ignore-path .gitignore --ext .ts,.tsx . --fix",
@@ -25,7 +25,7 @@
2525
"@mui/material": "^7.3.6",
2626
"@mui/x-data-grid": "^8.14.1",
2727
"@oceanprotocol/contracts": "2.6.0",
28-
"@oceanprotocol/lib": "8.0.6",
28+
"@oceanprotocol/lib": "^8.3.0",
2929
"@ramp-network/ramp-instant-sdk": "^6.2.0",
3030
"@tanstack/react-query": "^5.28.4",
3131
"@wagmi/core": "^2.15.0",
@@ -78,6 +78,7 @@
7878
},
7979
"packageManager": "yarn@4.5.1+sha512.341db9396b6e289fecc30cd7ab3af65060e05ebff4b3b47547b278b9e67b08f485ecd8c79006b405446262142c7a38154445ef7f17c1d5d1de7d90bf9ce7054d",
8080
"resolutions": {
81-
"axios": "1.12.2"
81+
"axios": "1.12.2",
82+
"uuid": "^8.3.2"
8283
}
8384
}

src/components/Navigation/profile-button.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useOceanAccount } from '@/lib/use-ocean-account';
55
import { GrantStatus } from '@/types/grant';
66
import { formatWalletAddress } from '@/utils/formatters';
77
import { useAuthModal, useLogout } from '@account-kit/react';
8+
import KeyIcon from '@mui/icons-material/Key';
89
import ListAltIcon from '@mui/icons-material/ListAlt';
910
import LogoutIcon from '@mui/icons-material/Logout';
1011
import PersonIcon from '@mui/icons-material/Person';
@@ -152,6 +153,18 @@ const ProfileButton: React.FC = () => {
152153
Manage access lists
153154
</MenuItem>
154155
)}
156+
<MenuItem
157+
disableRipple
158+
onClick={() => {
159+
router.push('/nodes/tokens');
160+
handleCloseMenu();
161+
}}
162+
>
163+
<ListItemIcon>
164+
<KeyIcon />
165+
</ListItemIcon>
166+
Node auth tokens
167+
</MenuItem>
155168
<MenuItem
156169
sx={{
157170
color: 'var(--error-darker)',
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.form {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 24px;
5+
}
6+
7+
.expiryControls {
8+
align-items: center;
9+
display: flex;
10+
gap: 8px;
11+
}
12+
13+
.unitSelect {
14+
background: transparent;
15+
border: none;
16+
color: var(--text-primary);
17+
cursor: pointer;
18+
font-family: var(--font-inter), sans-serif;
19+
font-size: 16px;
20+
font-weight: 500;
21+
padding: 4px 4px 4px 0;
22+
}
23+
24+
.unitSelect:focus {
25+
outline: none;
26+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use client';
2+
3+
import Button from '@/components/button/button';
4+
import Input from '@/components/input/input';
5+
import Modal from '@/components/modal/modal';
6+
import { useNodeTokensContext } from '@/context/node-tokens';
7+
import { useOceanAccount } from '@/lib/use-ocean-account';
8+
import { createAuthToken } from '@/services/nodeService';
9+
import { DURATION_UNIT_OPTIONS, type DurationUnit, toSeconds } from '@/utils/duration';
10+
import { useFormik } from 'formik';
11+
import posthog from 'posthog-js';
12+
import { toast } from 'react-toastify';
13+
import * as Yup from 'yup';
14+
import styles from './generate-token-modal.module.css';
15+
16+
type GenerateTokenModalProps = {
17+
isOpen: boolean;
18+
onClose: () => void;
19+
onTokenGenerated?: (token: string) => void;
20+
};
21+
22+
type FormValues = {
23+
nodeId: string;
24+
expiryValue: number | '';
25+
expiryUnit: DurationUnit;
26+
};
27+
28+
const GenerateTokenModal: React.FC<GenerateTokenModalProps> = ({ isOpen, onClose, onTokenGenerated }) => {
29+
const { account, signMessage } = useOceanAccount();
30+
const { addNodeToken } = useNodeTokensContext();
31+
32+
const formik = useFormik<FormValues>({
33+
initialValues: {
34+
nodeId: '',
35+
expiryValue: '',
36+
expiryUnit: 'hours',
37+
},
38+
validationSchema: Yup.object({
39+
nodeId: Yup.string().required('Node ID is required'),
40+
expiryValue: Yup.number().typeError('Must be a number').min(0, 'Must be 0 or greater'),
41+
}),
42+
onSubmit: async (values, { resetForm }) => {
43+
if (!account.address) {
44+
return;
45+
}
46+
try {
47+
const expirySeconds = values.expiryValue !== '' ? toSeconds(Number(values.expiryValue), values.expiryUnit) : 0;
48+
const validUntil = expirySeconds > 0 ? Date.now() + expirySeconds * 1000 : undefined;
49+
const { token: generatedToken } = await createAuthToken({
50+
consumerAddress: account.address,
51+
nodeUri: values.nodeId,
52+
signMessage,
53+
validUntil,
54+
});
55+
addNodeToken({
56+
createdAt: Date.now(),
57+
expiryTimestamp: validUntil,
58+
nodeId: values.nodeId,
59+
nodeUri: values.nodeId,
60+
token: generatedToken,
61+
});
62+
posthog.capture('authToken_generated', { nodeId: values.nodeId });
63+
toast.success('Auth token generated');
64+
resetForm();
65+
onTokenGenerated?.(generatedToken);
66+
onClose();
67+
} catch (error) {
68+
console.error('Failed to generate auth token:', error);
69+
toast.error('Failed to generate auth token');
70+
}
71+
},
72+
});
73+
74+
const handleClose = () => {
75+
if (formik.isSubmitting) {
76+
return;
77+
}
78+
formik.resetForm();
79+
onClose();
80+
};
81+
82+
const expiryError = formik.touched.expiryValue && formik.errors.expiryValue ? formik.errors.expiryValue : undefined;
83+
84+
return (
85+
<Modal
86+
hideCloseButton={formik.isSubmitting}
87+
isOpen={isOpen}
88+
onClose={handleClose}
89+
title="Generate auth token"
90+
width="sm"
91+
>
92+
<form className={styles.form} onSubmit={formik.handleSubmit}>
93+
<Input
94+
errorText={formik.touched.nodeId && formik.errors.nodeId ? formik.errors.nodeId : undefined}
95+
label="Node ID"
96+
name="nodeId"
97+
onBlur={formik.handleBlur}
98+
onChange={formik.handleChange}
99+
placeholder="Enter node peer ID"
100+
type="text"
101+
value={formik.values.nodeId}
102+
/>
103+
<Input
104+
endAdornment={
105+
<div className={styles.expiryControls}>
106+
<select
107+
aria-label="Expiration unit"
108+
className={styles.unitSelect}
109+
name="expiryUnit"
110+
onBlur={formik.handleBlur}
111+
onChange={formik.handleChange}
112+
value={formik.values.expiryUnit}
113+
>
114+
{DURATION_UNIT_OPTIONS.map((opt) => (
115+
<option key={opt.value} value={opt.value}>
116+
{opt.label}
117+
</option>
118+
))}
119+
</select>
120+
</div>
121+
}
122+
errorText={expiryError}
123+
label="Token expiration (optional)"
124+
min={0}
125+
name="expiryValue"
126+
onBlur={formik.handleBlur}
127+
onChange={(e) => {
128+
if (e.target.value === '') {
129+
formik.setFieldValue('expiryValue', '');
130+
return;
131+
}
132+
formik.setFieldValue('expiryValue', Math.max(0, Number(e.target.value)));
133+
}}
134+
type="number"
135+
value={formik.values.expiryValue}
136+
/>
137+
<div className="actionsGroupMdEnd">
138+
<Button
139+
color="accent1"
140+
disabled={formik.isSubmitting}
141+
onClick={handleClose}
142+
size="md"
143+
type="button"
144+
variant="outlined"
145+
>
146+
Cancel
147+
</Button>
148+
<Button color="accent1" disabled={!formik.isValid} loading={formik.isSubmitting} size="md" type="submit">
149+
Generate
150+
</Button>
151+
</div>
152+
</form>
153+
</Modal>
154+
);
155+
};
156+
157+
export default GenerateTokenModal;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import NodesTokens from '@/components/node-tokens/nodes-tokens';
2+
3+
const NodeTokensPage: React.FC = () => {
4+
return <NodesTokens />;
5+
};
6+
7+
export default NodeTokensPage;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.tokenValue {
2+
font-family: var(--font-monospace, monospace);
3+
font-size: 13px;
4+
word-break: break-all;
5+
}
6+
7+
.empty {
8+
font-size: 13px;
9+
color: var(--text-secondary);
10+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client';
2+
3+
import Button from '@/components/button/button';
4+
import CopyButton from '@/components/button/copy-button';
5+
import { Table } from '@/components/table/table';
6+
import { TableTypeEnum } from '@/components/table/table-type';
7+
import { NodeToken } from '@/types/node-tokens';
8+
import { formatDateTime, formatWalletAddress } from '@/utils/formatters';
9+
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
10+
import { GridColDef } from '@mui/x-data-grid';
11+
import classNames from 'classnames';
12+
import styles from './node-tokens.module.css';
13+
14+
type NodeTokensProps = {
15+
nodeId: string;
16+
tokens: NodeToken[];
17+
onRemove: (token: NodeToken) => void;
18+
};
19+
20+
const columns: GridColDef<NodeToken>[] = [
21+
{
22+
field: 'token',
23+
headerName: 'Key',
24+
flex: 1,
25+
minWidth: 180,
26+
sortable: false,
27+
renderCell: ({ row }) => formatWalletAddress(row.token),
28+
},
29+
{
30+
field: 'active',
31+
headerName: 'Status',
32+
width: 110,
33+
sortable: false,
34+
renderCell: ({ row }) => {
35+
const expired = !!row.expiryTimestamp && row.expiryTimestamp < Date.now();
36+
return (
37+
<span className={classNames('chip', expired ? 'chipError' : 'chipSuccess')} style={{ alignSelf: 'center' }}>
38+
{expired ? 'Expired' : 'Active'}
39+
</span>
40+
);
41+
},
42+
},
43+
{
44+
field: 'createdAt',
45+
headerName: 'Created',
46+
width: 180,
47+
sortable: false,
48+
renderCell: ({ row }) => formatDateTime(row.createdAt / 1000),
49+
},
50+
{
51+
field: 'expiryTimestamp',
52+
headerName: 'Expiration date',
53+
width: 180,
54+
sortable: false,
55+
renderCell: ({ row }) => (row.expiryTimestamp ? formatDateTime(row.expiryTimestamp / 1000) : 'No expiration'),
56+
},
57+
];
58+
59+
const NodeTokens: React.FC<NodeTokensProps> = ({ tokens, onRemove }) => {
60+
if (tokens?.length === 0) {
61+
return <span className={styles.empty}>No tokens generated for this node yet.</span>;
62+
}
63+
64+
return (
65+
<Table<NodeToken>
66+
autoHeight
67+
initialDensity="compact"
68+
actionsColumn={({ row }) => (
69+
<>
70+
<CopyButton color="accent1" contentToCopy={row.token} size="sm" variant="transparent" />
71+
<Button
72+
autoLoading
73+
color="accent1"
74+
contentBefore={<DeleteOutlineIcon />}
75+
onClick={() => onRemove(row)}
76+
size="sm"
77+
variant="transparent"
78+
>
79+
Revoke
80+
</Button>
81+
</>
82+
)}
83+
columns={columns}
84+
data={tokens}
85+
getRowId={(row) => row.token}
86+
paginationType="none"
87+
tableType={TableTypeEnum.MY_JOBS}
88+
/>
89+
);
90+
};
91+
92+
export default NodeTokens;

0 commit comments

Comments
 (0)