Skip to content

Commit d71cc39

Browse files
authored
feat: Update api key setup and account pages/settings
2 parents 877b7c0 + 981e7c7 commit d71cc39

40 files changed

Lines changed: 4824 additions & 325 deletions

Caddyfile

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ www.underlay.org {
1010
handle /api/* {
1111
reverse_proxy 127.0.0.1:3001
1212
}
13-
handle /assets/* {
14-
reverse_proxy 127.0.0.1:3001
15-
}
1613
handle /uploads/* {
1714
reverse_proxy 127.0.0.1:3001
1815
}
@@ -30,9 +27,6 @@ dev.underlay.org {
3027
handle /api/* {
3128
reverse_proxy 127.0.0.1:3000
3229
}
33-
handle /assets/* {
34-
reverse_proxy 127.0.0.1:3000
35-
}
3630
handle /uploads/* {
3731
reverse_proxy 127.0.0.1:3000
3832
}

docker-compose.local.yml

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,35 +35,6 @@ services:
3535
memory: 1g
3636
cpus: "1.0"
3737

38-
minio:
39-
image: minio/minio:latest
40-
command: server /data --console-address ":9001"
41-
environment:
42-
MINIO_ROOT_USER: minioadmin
43-
MINIO_ROOT_PASSWORD: minioadmin
44-
ports:
45-
- "9000:9000"
46-
- "9001:9001"
47-
volumes:
48-
- miniodata-dev:/data
49-
networks:
50-
- dev
51-
52-
createbucket:
53-
image: minio/mc:latest
54-
depends_on:
55-
minio:
56-
condition: service_started
57-
networks:
58-
- dev
59-
entrypoint: >
60-
/bin/sh -c "
61-
sleep 2;
62-
mc alias set local http://minio:9000 minioadmin minioadmin;
63-
mc mb local/underlay --ignore-existing;
64-
exit 0;
65-
"
66-
6738
app:
6839
build:
6940
context: .
@@ -79,8 +50,6 @@ services:
7950
depends_on:
8051
postgres:
8152
condition: service_healthy
82-
minio:
83-
condition: service_started
8453
volumes:
8554
- ./src:/app/src
8655
- ./public:/app/public
@@ -107,4 +76,3 @@ networks:
10776

10877
volumes:
10978
pgdata-dev:
110-
miniodata-dev:

package-lock.json

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@fastify/cookie": "^11.0.0",
3333
"@fastify/cors": "^11.0.0",
3434
"@fastify/multipart": "^10.0.0",
35+
"@fastify/rate-limit": "^10.3.0",
3536
"@lucide/astro": "^1.11.0",
3637
"@sinclair/typebox": "^0.34.0",
3738
"ajv": "^8.17.0",
@@ -43,6 +44,7 @@
4344
"lucide-react": "^1.11.0",
4445
"marked": "^18.0.0",
4546
"node-cron": "^4.0.0",
47+
"nodemailer": "^8.0.7",
4648
"postgres": "^3.4.0",
4749
"react": "^19.1.0",
4850
"react-dom": "^19.1.0",
@@ -53,6 +55,7 @@
5355
"@tailwindcss/vite": "^4.1.0",
5456
"@types/bcrypt": "^6.0.0",
5557
"@types/node": "^25.0.0",
58+
"@types/nodemailer": "^8.0.0",
5659
"@types/react": "^19.1.0",
5760
"@types/react-dom": "^19.1.0",
5861
"@types/tar-stream": "^3.1.4",

public/.well-known/ai.txt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,26 @@ There are two auth methods:
2323
POST /api/accounts/logout clears the session.
2424
GET /api/accounts/me returns the current user (works with either auth method).
2525

26-
Public endpoints (browsing collections, reading public versions) require no auth.
27-
Write endpoints require a key with "write" scope or an active session.
28-
Delete endpoints require "admin" scope.
26+
All GET requests are public — no auth required to read public data.
27+
All write requests (POST, PATCH, PUT, DELETE) require authentication.
28+
If a Bearer token is provided but invalid, the request is rejected immediately (401).
29+
30+
## Rate Limits
31+
32+
Requests are rate-limited per IP (unauthenticated) or per account (authenticated).
33+
34+
| Auth status | Limit |
35+
|-------------------|---------------|
36+
| Unauthenticated | 60 req/min |
37+
| Authenticated | 5,000 req/min |
38+
39+
Rate limit headers are included on every response:
40+
- X-RateLimit-Limit: max requests in the window
41+
- X-RateLimit-Remaining: requests left
42+
- X-RateLimit-Reset: seconds until the window resets
43+
44+
When exceeded, you'll get a 429 response with a Retry-After header.
45+
To get the higher limit, authenticate with an API key (recommended for any automated access).
2946

3047
---
3148

src/api/plugins/auth.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,36 @@ declare module "fastify" {
1414
}
1515

1616
async function authPlugin(app: FastifyInstance) {
17-
// API key auth via Bearer token
1817
app.decorateRequest("accountId", undefined);
1918
app.decorateRequest("apiKeyScope", undefined);
2019
app.decorateRequest("apiKeyCollectionId", undefined);
2120
app.decorateRequest("sessionUserId", undefined);
2221

23-
app.addHook("onRequest", async (request: FastifyRequest) => {
22+
// Paths that never require auth (even for POST)
23+
const publicPaths = new Set([
24+
"/api/health",
25+
"/api/accounts/signup",
26+
"/api/accounts/login",
27+
"/api/accounts/forgot-password",
28+
"/api/accounts/reset-password",
29+
]);
30+
31+
const internalToken = process.env.INTERNAL_API_TOKEN ?? "internal-dev-token";
32+
33+
app.addHook("onRequest", async (request: FastifyRequest, reply: FastifyReply) => {
34+
// Internal service calls from Astro SSR
35+
const internalHeader = request.headers["x-internal-token"];
36+
if (internalHeader === internalToken) {
37+
request.apiKeyScope = "read";
38+
return;
39+
}
40+
41+
// API key auth via Bearer token
2442
const auth = request.headers.authorization;
2543
if (auth?.startsWith("Bearer ")) {
2644
const token = auth.slice(7);
2745
const keys = await db.select().from(schema.apiKeys);
46+
let matched = false;
2847
for (const key of keys) {
2948
const match = await bcrypt.compare(token, key.keyHash);
3049
if (match) {
@@ -35,9 +54,14 @@ async function authPlugin(app: FastifyInstance) {
3554
.update(schema.apiKeys)
3655
.set({ lastUsedAt: new Date() })
3756
.where(eq(schema.apiKeys.id, key.id));
38-
return;
57+
matched = true;
58+
break;
3959
}
4060
}
61+
if (!matched) {
62+
return reply.status(401).send({ error: "Invalid API key", statusCode: 401 });
63+
}
64+
return;
4165
}
4266

4367
// Session cookie auth
@@ -62,6 +86,16 @@ async function authPlugin(app: FastifyInstance) {
6286
// Invalid or expired cookie — ignore silently
6387
}
6488
}
89+
90+
// Public GETs are allowed without auth (rate-limited by IP)
91+
if (request.method === "GET") return;
92+
93+
// All writes (POST/PATCH/PUT/DELETE) require auth, except public paths
94+
if (!request.accountId) {
95+
const path = request.url.split("?")[0];
96+
if (publicPaths.has(path)) return;
97+
return reply.status(401).send({ error: "Authentication required", statusCode: 401 });
98+
}
6599
});
66100
}
67101

0 commit comments

Comments
 (0)