Skip to content

Commit 077f9f2

Browse files
authored
Merge pull request #73 from namestonehq/darian/admin_apis
Darian/admin apis
2 parents bac0882 + db7c697 commit 077f9f2

File tree

9 files changed

+1024
-0
lines changed

9 files changed

+1024
-0
lines changed

data/docs/get-domains.mdx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Get Domains
2+
3+
This GET route retrieves all domains where the specified address is an admin. It returns information about each domain including address, contenthash, text records, and coin types.
4+
5+
## Parameters
6+
7+
| Parameter | Type | Required | Description |
8+
| --------------- | ------ | -------- | ----------------------------------------------------------------------------------- |
9+
| `admin-address` | string | Yes | The Ethereum address that is an admin of the domains to retrieve. |
10+
| `limit` | number | No | The number of domains to return (default: 50, max: 1000). |
11+
| `offset` | number | No | The number of domains to skip (default: 0). |
12+
| `text_records` | string | No | Set to "0" to exclude text records and coin types from the response (default: "1"). |
13+
14+
## Response
15+
16+
The response is an array of domain objects with the following properties:
17+
18+
| Property | Type | Description |
19+
| -------------- | ------ | ------------------------------------------------------------------------------- |
20+
| `domain` | string | The domain name (e.g., "namestone.xyz"). |
21+
| `address` | string | The Ethereum address the domain resolves to. |
22+
| `contenthash` | string | The IPFS or IPNS contenthash for the domain's website, if set. |
23+
| `text_records` | object | An object containing key-value pairs of the domain's text records (if included).|
24+
| `coin_types` | object | An object containing key-value pairs of L2 chains and their addresses (if included). |
25+
26+
## Error Codes
27+
28+
| Status Code | Description |
29+
| ----------- | ------------------------------------------------- |
30+
| 400 | Bad request. Invalid parameters or network. |
31+
| 500 | Server error. |
32+
33+
## Curl Example
34+
35+
```
36+
curl -X GET \
37+
-H 'Content-Type: application/json' \
38+
'https://namestone.com/api/public_v1/get-domains?admin-address=0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF'
39+
```
40+
41+
## Example Response
42+
43+
```json
44+
[
45+
{
46+
"domain": "namestone.xyz",
47+
"address": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF",
48+
"contenthash": "ipfs://QmUbTVz1L4uEvAPg5QcSu8Rifq2CtTc4SYmasXLAYkFQbp",
49+
"text_records": {
50+
"com.twitter": "namestonehq",
51+
"com.github": "resolverworks",
52+
"url": "https://www.namestone.xyz",
53+
"description": "Namestone ENS Resolver"
54+
},
55+
"coin_types": {
56+
"60": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF",
57+
"2147483785": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF"
58+
}
59+
},
60+
{
61+
"domain": "example.eth",
62+
"address": "0xA47632346786AD59c8590Bd4898D84B4eAB97644",
63+
"contenthash": null,
64+
"text_records": {
65+
"description": "Example domain"
66+
},
67+
"coin_types": {}
68+
}
69+
]
70+
```
71+
72+
This endpoint is particularly useful for domain administrators who need to manage multiple domains and want to retrieve a comprehensive list of all domains under their administration.

jest.env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import dotenv from "dotenv";
2+
dotenv.config({ path: ".env.test" });

pages/api/[network]/add-admin.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import sql from "../../../lib/db";
2+
import {
3+
checkApiKey,
4+
getNetwork,
5+
getClientIp,
6+
} from "../../../utils/ServerUtils";
7+
import Cors from "micro-cors";
8+
import { normalize } from "viem/ens";
9+
10+
const cors = Cors({
11+
allowMethods: ["GET", "HEAD", "POST"],
12+
origin: "*",
13+
});
14+
15+
async function handler(req, res) {
16+
const network = getNetwork(req);
17+
if (!network) {
18+
return res.status(400).json({ error: "Invalid network" });
19+
}
20+
const { headers } = req;
21+
22+
// Check required parameters
23+
let body = req.body;
24+
if (typeof body === "string") {
25+
body = JSON.parse(body);
26+
}
27+
if (!body.domain) {
28+
return res.status(400).json({ error: "Missing domain" });
29+
}
30+
if (!body.admin_address) {
31+
return res.status(400).json({ error: "Missing admin_address" });
32+
}
33+
34+
let domain;
35+
let admin_address = body.admin_address;
36+
try {
37+
domain = normalize(body.domain);
38+
} catch (e) {
39+
return res.status(400).json({ error: "Invalid ens domain" });
40+
}
41+
42+
// Check API key
43+
const allowedApi = await checkApiKey(
44+
headers.authorization || req.query.api_key,
45+
domain
46+
);
47+
if (!allowedApi) {
48+
return res
49+
.status(401)
50+
.json({ error: "You are not authorized to use this endpoint" });
51+
}
52+
53+
try {
54+
// Check if domain exists
55+
const domainQuery = await sql`
56+
select id from domain where name = ${domain} and network = ${network} limit 1`;
57+
58+
if (domainQuery.length === 0) {
59+
return res.status(400).json({ error: "Domain does not exist" });
60+
}
61+
62+
const domainId = domainQuery[0].id;
63+
64+
// Insert admin to admin table
65+
await sql`
66+
insert into admin (domain_id, address)
67+
values (${domainId}, ${admin_address})
68+
on conflict (domain_id, address) do nothing;
69+
`;
70+
} catch (error) {
71+
console.error("Error adding admin:", error);
72+
return res.status(500).json({ error: "Internal server error" });
73+
}
74+
75+
// log user engagement
76+
const clientIp = getClientIp(req);
77+
const jsonPayload = JSON.stringify({
78+
body: body,
79+
ip_address: clientIp,
80+
});
81+
await sql`
82+
insert into user_engagement (address, name, details)
83+
values (${admin_address},'add_admin', ${jsonPayload})`;
84+
85+
return res.status(200).json({ success: true });
86+
}
87+
88+
export default cors(handler);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { createRequest, createResponse } from "node-mocks-http";
2+
import handler from "./add-admin";
3+
import sqlForTests from "../../../test_utils/mock_db";
4+
import {
5+
setupTestDatabase,
6+
teardownTestDatabase,
7+
} from "../../../test_utils/test_db_setup";
8+
9+
const TEST_DOMAIN = "test-admin.eth";
10+
const TEST_ADMIN_ADDRESS = "0xAdminTestAddress123456789012345678901234";
11+
const TEST_API_KEY = "fake-test-api-key";
12+
const TEST_NETWORK = "mainnet";
13+
14+
describe("add-admin API E2E", () => {
15+
let domainId;
16+
17+
beforeAll(async () => {
18+
process.env.TEST_API_KEY = TEST_API_KEY;
19+
await setupTestDatabase();
20+
// Insert domain
21+
const [domain] = await sqlForTests`
22+
INSERT INTO domain (name, network) VALUES (${TEST_DOMAIN}, ${TEST_NETWORK}) RETURNING id
23+
`;
24+
domainId = domain.id;
25+
// Insert API key
26+
await sqlForTests`
27+
INSERT INTO api_key (domain_id, key) VALUES (${domainId}, ${TEST_API_KEY})
28+
`;
29+
});
30+
31+
afterAll(async () => {
32+
await sqlForTests`DELETE FROM admin WHERE domain_id = ${domainId}`;
33+
await sqlForTests`DELETE FROM api_key WHERE domain_id = ${domainId}`;
34+
await sqlForTests`DELETE FROM domain WHERE id = ${domainId}`;
35+
await teardownTestDatabase();
36+
});
37+
38+
test("successfully adds an admin", async () => {
39+
try {
40+
const req = createRequest({
41+
method: "POST",
42+
headers: { authorization: TEST_API_KEY },
43+
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
44+
query: { network: "public_v1" },
45+
});
46+
const res = createResponse();
47+
await handler(req, res);
48+
expect(res._getStatusCode()).toBe(200);
49+
expect(JSON.parse(res._getData())).toEqual({ success: true });
50+
// Check DB
51+
const admins =
52+
await sqlForTests`SELECT * FROM admin WHERE domain_id = ${domainId} AND address = ${TEST_ADMIN_ADDRESS}`;
53+
expect(admins.length).toBe(1);
54+
} catch (err) {
55+
console.error("Test error (successfully adds an admin):", err);
56+
throw err;
57+
}
58+
});
59+
60+
test("missing domain returns 400", async () => {
61+
const req = createRequest({
62+
method: "POST",
63+
headers: { authorization: TEST_API_KEY },
64+
body: { admin_address: TEST_ADMIN_ADDRESS },
65+
query: { network: "public_v1" },
66+
});
67+
const res = createResponse();
68+
await handler(req, res);
69+
expect(res._getStatusCode()).toBe(400);
70+
expect(JSON.parse(res._getData())).toEqual({ error: "Missing domain" });
71+
});
72+
73+
test("missing admin_address returns 400", async () => {
74+
const req = createRequest({
75+
method: "POST",
76+
headers: { authorization: TEST_API_KEY },
77+
body: { domain: TEST_DOMAIN },
78+
query: { network: "public_v1" },
79+
});
80+
const res = createResponse();
81+
await handler(req, res);
82+
expect(res._getStatusCode()).toBe(400);
83+
expect(JSON.parse(res._getData())).toEqual({
84+
error: "Missing admin_address",
85+
});
86+
});
87+
88+
test("invalid domain returns 400", async () => {
89+
const req = createRequest({
90+
method: "POST",
91+
headers: { authorization: TEST_API_KEY },
92+
body: { domain: "invalid domain!@#", admin_address: TEST_ADMIN_ADDRESS },
93+
query: { network: "public_v1" },
94+
});
95+
const res = createResponse();
96+
await handler(req, res);
97+
expect(res._getStatusCode()).toBe(400);
98+
expect(JSON.parse(res._getData())).toEqual({ error: "Invalid ens domain" });
99+
});
100+
101+
test("unauthorized API key returns 401", async () => {
102+
const req = createRequest({
103+
method: "POST",
104+
headers: { authorization: "wrong-key" },
105+
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
106+
query: { network: "public_v1" },
107+
});
108+
const res = createResponse();
109+
await handler(req, res);
110+
expect(res._getStatusCode()).toBe(401);
111+
expect(JSON.parse(res._getData())).toEqual({
112+
error: "You are not authorized to use this endpoint",
113+
});
114+
});
115+
116+
test("duplicate admin does not error", async () => {
117+
// Add once
118+
await sqlForTests`INSERT INTO admin (domain_id, address) VALUES (${domainId}, ${TEST_ADMIN_ADDRESS}) ON CONFLICT DO NOTHING`;
119+
// Add again via API
120+
const req = createRequest({
121+
method: "POST",
122+
headers: { authorization: TEST_API_KEY },
123+
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
124+
query: { network: "public_v1" },
125+
});
126+
const res = createResponse();
127+
await handler(req, res);
128+
expect(res._getStatusCode()).toBe(200);
129+
expect(JSON.parse(res._getData())).toEqual({ success: true });
130+
});
131+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import sql from "../../../lib/db";
2+
import {
3+
checkApiKey,
4+
getNetwork,
5+
getClientIp,
6+
} from "../../../utils/ServerUtils";
7+
import Cors from "micro-cors";
8+
import { normalize } from "viem/ens";
9+
10+
const cors = Cors({
11+
allowMethods: ["GET", "HEAD", "POST"],
12+
origin: "*",
13+
});
14+
15+
async function handler(req, res) {
16+
const network = getNetwork(req);
17+
if (!network) {
18+
return res.status(400).json({ error: "Invalid network" });
19+
}
20+
const { headers } = req;
21+
22+
// Check required parameters
23+
let body = req.body;
24+
if (typeof body === "string") {
25+
body = JSON.parse(body);
26+
}
27+
if (!body.domain) {
28+
return res.status(400).json({ error: "Missing domain" });
29+
}
30+
if (!body.admin_address) {
31+
return res.status(400).json({ error: "Missing admin_address" });
32+
}
33+
34+
let domain;
35+
let admin_address = body.admin_address;
36+
try {
37+
domain = normalize(body.domain);
38+
} catch (e) {
39+
return res.status(400).json({ error: "Invalid ens domain" });
40+
}
41+
42+
// Check API key
43+
const allowedApi = await checkApiKey(
44+
headers.authorization || req.query.api_key,
45+
domain
46+
);
47+
if (!allowedApi) {
48+
return res
49+
.status(401)
50+
.json({ error: "You are not authorized to use this endpoint" });
51+
}
52+
53+
try {
54+
// Check if domain exists
55+
const domainQuery = await sql`
56+
select id from domain where name = ${domain} and network = ${network} limit 1`;
57+
58+
if (domainQuery.length === 0) {
59+
return res.status(400).json({ error: "Domain does not exist" });
60+
}
61+
62+
const domainId = domainQuery[0].id;
63+
64+
// Delete admin from admin table
65+
const result = await sql`
66+
delete from admin where domain_id = ${domainId} and address = ${admin_address};
67+
`;
68+
69+
if (result.count === 0) {
70+
return res.status(404).json({ error: "Admin not found for this domain" });
71+
}
72+
} catch (error) {
73+
console.error("Error deleting admin:", error);
74+
return res.status(500).json({ error: "Internal server error" });
75+
}
76+
77+
// log user engagement
78+
const clientIp = getClientIp(req);
79+
const jsonPayload = JSON.stringify({
80+
body: body,
81+
ip_address: clientIp,
82+
});
83+
await sql`
84+
insert into user_engagement (address, name, details)
85+
values (${admin_address},'delete_admin', ${jsonPayload})`;
86+
87+
return res.status(200).json({ success: true });
88+
}
89+
90+
export default cors(handler);

0 commit comments

Comments
 (0)