Skip to content

Commit 8d11e53

Browse files
authored
Adds auth (#174)
* adds fastintear auth, init * auth flow * fmt * adds fastintear auth, init * auth flow * fmt * frontend auth * auth middleware * feed protection * fmt * moderation wip * update lock * migration
1 parent 79f280d commit 8d11e53

File tree

82 files changed

+5059
-7337
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+5059
-7337
lines changed

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"lodash": "^4.17.21",
6060
"mustache": "^4.2.0",
6161
"near-api-js": "^5.1.1",
62+
"near-sign-verify": "^0.3.6",
6263
"ora": "^8.1.1",
6364
"pg": "^8.15.6",
6465
"pinata-web3": "^0.5.4",

apps/api/src/app.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { secureHeaders } from "hono/secure-headers";
44
import { db } from "./db";
55
import { apiRoutes } from "./routes/api";
66
import { AppInstance, Env } from "./types/app";
7-
import { web3AuthJwtMiddleware } from "./utils/auth";
87
import { getAllowedOrigins } from "./utils/config";
98
import { errorHandler } from "./utils/error";
109
import { ServiceProvider } from "./utils/service-provider";
10+
import { logger } from "utils/logger";
11+
import { createAuthMiddleware } from "./middlewares/auth.middleware";
1112

1213
const ALLOWED_ORIGINS = getAllowedOrigins();
1314

@@ -18,7 +19,7 @@ export async function createApp(): Promise<AppInstance> {
1819
const app = new Hono<Env>();
1920

2021
app.onError((err, c) => {
21-
return errorHandler(err, c);
22+
return errorHandler(err, c, logger);
2223
});
2324

2425
app.use(
@@ -44,8 +45,8 @@ export async function createApp(): Promise<AppInstance> {
4445
await next();
4546
});
4647

47-
// Authentication middleware
48-
app.use("*", web3AuthJwtMiddleware);
48+
// Apply auth middleware to all /api routes
49+
app.use("/api/*", createAuthMiddleware());
4950

5051
// Mount API routes
5152
app.route("/api", apiRoutes);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Context, MiddlewareHandler, Next } from "hono";
2+
import { verify } from "near-sign-verify";
3+
4+
export function createAuthMiddleware(): MiddlewareHandler {
5+
return async (c: Context, next: Next) => {
6+
const method = c.req.method;
7+
let accountId: string | null = null;
8+
9+
if (method === "GET") {
10+
const nearAccountHeader = c.req.header("X-Near-Account");
11+
if (
12+
nearAccountHeader &&
13+
nearAccountHeader.toLowerCase() !== "anonymous"
14+
) {
15+
accountId = nearAccountHeader;
16+
}
17+
// If header is missing or "anonymous", accountId remains null
18+
c.set("accountId", accountId);
19+
await next();
20+
return;
21+
}
22+
23+
// For non-GET requests (POST, PUT, DELETE, PATCH, etc.)
24+
const authHeader = c.req.header("Authorization");
25+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
26+
c.status(401);
27+
return c.json({
28+
error: "Unauthorized",
29+
details: "Missing or malformed Authorization header.",
30+
});
31+
}
32+
33+
const token = authHeader.substring(7); // Remove "Bearer "
34+
35+
try {
36+
const verificationResult = await verify(token, {
37+
expectedRecipient: "curatefun.near",
38+
requireFullAccessKey: false,
39+
nonceMaxAge: 300000, // 5 mins
40+
});
41+
42+
accountId = verificationResult.accountId;
43+
c.set("accountId", accountId);
44+
await next();
45+
} catch (error) {
46+
console.error("Token verification error:", error);
47+
c.status(401);
48+
return c.json({
49+
error: "Unauthorized",
50+
details: "Invalid token signature or recipient.",
51+
});
52+
}
53+
};
54+
}

apps/api/src/routes/api/feeds.ts

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,49 @@ feedsRoutes.get("/", async (c) => {
2525
* Create a new feed
2626
*/
2727
feedsRoutes.post("/", async (c) => {
28+
const accountId = c.get("accountId");
29+
if (!accountId) {
30+
return c.json(
31+
{ error: "Unauthorized. User must be logged in to create a feed." },
32+
401,
33+
);
34+
}
35+
2836
const body = await c.req.json();
29-
const validationResult = insertFeedSchema.safeParse(body);
37+
const partialValidationResult = insertFeedSchema
38+
.omit({ created_by: true })
39+
.safeParse(body);
3040

31-
if (!validationResult.success) {
32-
return badRequest(c, "Invalid feed data", validationResult.error.flatten());
41+
if (!partialValidationResult.success) {
42+
return badRequest(
43+
c,
44+
"Invalid feed data",
45+
partialValidationResult.error.flatten(),
46+
);
47+
}
48+
49+
const feedDataWithCreator = {
50+
...partialValidationResult.data,
51+
created_by: accountId,
52+
};
53+
54+
const finalValidationResult = insertFeedSchema.safeParse(feedDataWithCreator);
55+
if (!finalValidationResult.success) {
56+
logger.error(
57+
"Error in final validation after adding created_by",
58+
finalValidationResult.error,
59+
);
60+
return badRequest(
61+
c,
62+
"Internal validation error",
63+
finalValidationResult.error.flatten(),
64+
);
3365
}
3466

3567
const sp = c.get("sp");
3668
const feedService = sp.getFeedService();
3769
try {
38-
const newFeed = await feedService.createFeed(validationResult.data);
70+
const newFeed = await feedService.createFeed(finalValidationResult.data);
3971
return c.json(newFeed, 201);
4072
} catch (error) {
4173
logger.error("Error creating feed:", error);
@@ -66,16 +98,37 @@ feedsRoutes.get("/:feedId", async (c) => {
6698
* Update an existing feed
6799
*/
68100
feedsRoutes.put("/:feedId", async (c) => {
101+
const accountId = c.get("accountId");
102+
if (!accountId) {
103+
return c.json(
104+
{ error: "Unauthorized. User must be logged in to update a feed." },
105+
401,
106+
);
107+
}
108+
69109
const feedId = c.req.param("feedId");
110+
const sp = c.get("sp");
111+
const feedService = sp.getFeedService();
112+
113+
const canUpdate = await feedService.hasPermission(
114+
accountId,
115+
feedId,
116+
"update",
117+
);
118+
if (!canUpdate) {
119+
return c.json(
120+
{ error: "Forbidden. You do not have permission to update this feed." },
121+
403,
122+
);
123+
}
124+
70125
const body = await c.req.json();
71126
const validationResult = updateFeedSchema.safeParse(body);
72127

73128
if (!validationResult.success) {
74129
return badRequest(c, "Invalid feed data", validationResult.error.flatten());
75130
}
76131

77-
const sp = c.get("sp");
78-
const feedService = sp.getFeedService();
79132
try {
80133
const updatedFeed = await feedService.updateFeed(
81134
feedId,
@@ -97,6 +150,14 @@ feedsRoutes.put("/:feedId", async (c) => {
97150
* Example: /api/feeds/solana/process?distributors=@curatedotfun/rss
98151
*/
99152
feedsRoutes.post("/:feedId/process", async (c) => {
153+
const accountId = c.get("accountId");
154+
if (!accountId) {
155+
return c.json(
156+
{ error: "Unauthorized. User must be logged in to process a feed." },
157+
401,
158+
);
159+
}
160+
100161
const sp = c.get("sp");
101162
const feedService = sp.getFeedService();
102163

@@ -127,9 +188,30 @@ feedsRoutes.post("/:feedId/process", async (c) => {
127188
* Delete a specific feed by its ID
128189
*/
129190
feedsRoutes.delete("/:feedId", async (c) => {
191+
const accountId = c.get("accountId");
192+
if (!accountId) {
193+
return c.json(
194+
{ error: "Unauthorized. User must be logged in to delete a feed." },
195+
401,
196+
);
197+
}
198+
130199
const feedId = c.req.param("feedId");
131200
const sp = c.get("sp");
132201
const feedService = sp.getFeedService();
202+
203+
const canDelete = await feedService.hasPermission(
204+
accountId,
205+
feedId,
206+
"delete",
207+
);
208+
if (!canDelete) {
209+
return c.json(
210+
{ error: "Forbidden. You do not have permission to delete this feed." },
211+
403,
212+
);
213+
}
214+
133215
try {
134216
const result = await feedService.deleteFeed(feedId);
135217
if (!result) {

apps/api/src/routes/api/users.ts

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,34 @@
11
import { zValidator } from "@hono/zod-validator";
22
import { Hono } from "hono";
33
import { ContentfulStatusCode } from "hono/utils/http-status";
4-
import { InsertUserData } from "../../services/users.service";
54
import { Env } from "../../types/app";
65
import {
76
NearAccountError,
87
NotFoundError,
98
UserServiceError,
109
} from "../../types/errors";
1110
import { ServiceProvider } from "../../utils/service-provider";
12-
import {
13-
insertUserSchema,
14-
updateUserSchema,
15-
} from "../../validation/users.validation";
11+
import { insertUserSchema, updateUserSchema } from "@curatedotfun/shared-db";
1612

1713
const usersRoutes = new Hono<Env>();
1814

1915
// --- GET /api/users/me ---
2016
usersRoutes.get("/me", async (c) => {
21-
const jwtPayload = c.get("jwtPayload");
22-
const authProviderId = jwtPayload?.authProviderId;
17+
const accountId = c.get("accountId");
2318

24-
if (!authProviderId) {
25-
return c.json(
26-
{ error: "Unauthorized: Missing or invalid authentication token" },
27-
401,
28-
);
19+
if (!accountId) {
20+
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
2921
}
3022

3123
try {
3224
const userService = ServiceProvider.getInstance().getUserService();
33-
const user = await userService.findUserByAuthProviderId(authProviderId);
25+
const user = await userService.findUserByNearAccountId(accountId);
3426

3527
if (!user) {
36-
return c.json({ error: "User profile not found" }, 404);
28+
return c.json(
29+
{ error: "User profile not found for the given NEAR account ID." },
30+
404,
31+
);
3732
}
3833

3934
return c.json({ profile: user });
@@ -46,23 +41,11 @@ usersRoutes.get("/me", async (c) => {
4641
// --- POST /api/users ---
4742
usersRoutes.post("/", zValidator("json", insertUserSchema), async (c) => {
4843
const createUserData = c.req.valid("json");
49-
const jwtPayload = c.get("jwtPayload");
50-
const authProviderId = jwtPayload?.authProviderId;
51-
52-
if (!authProviderId) {
53-
return c.json(
54-
{ error: "Unauthorized: Missing or invalid authentication token" },
55-
401,
56-
);
57-
}
5844

5945
try {
6046
const userService = ServiceProvider.getInstance().getUserService();
6147

62-
const newUser = await userService.createUser({
63-
auth_provider_id: authProviderId,
64-
...createUserData,
65-
} as InsertUserData);
48+
const newUser = await userService.createUser(createUserData);
6649

6750
return c.json({ profile: newUser }, 201);
6851
} catch (error: any) {
@@ -89,20 +72,16 @@ usersRoutes.post("/", zValidator("json", insertUserSchema), async (c) => {
8972
// --- PUT /api/users/me ---
9073
usersRoutes.put("/me", zValidator("json", updateUserSchema), async (c) => {
9174
const updateData = c.req.valid("json");
92-
const jwtPayload = c.get("jwtPayload");
93-
const authProviderId = jwtPayload?.authProviderId;
75+
const accountId = c.get("accountId");
9476

95-
if (!authProviderId) {
96-
return c.json(
97-
{ error: "Unauthorized: Missing or invalid authentication token" },
98-
401,
99-
);
77+
if (!accountId) {
78+
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
10079
}
10180

10281
try {
10382
const userService = ServiceProvider.getInstance().getUserService();
104-
const updatedUser = await userService.updateUser(
105-
authProviderId,
83+
const updatedUser = await userService.updateUserByNearAccountId(
84+
accountId,
10685
updateData,
10786
);
10887

@@ -134,24 +113,19 @@ usersRoutes.put("/me", zValidator("json", updateUserSchema), async (c) => {
134113

135114
// --- DELETE /api/users/me ---
136115
usersRoutes.delete("/me", async (c) => {
137-
const jwtPayload = c.get("jwtPayload");
138-
const authProviderId = jwtPayload?.authProviderId;
116+
const accountId = c.get("accountId");
139117

140-
if (!authProviderId) {
141-
return c.json(
142-
{ error: "Unauthorized: Missing or invalid authentication token" },
143-
401,
144-
);
118+
if (!accountId) {
119+
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
145120
}
146121

147122
try {
148123
const userService = ServiceProvider.getInstance().getUserService();
149-
const success = await userService.deleteUser(authProviderId);
124+
const success = await userService.deleteUserByNearAccountId(accountId);
150125

151126
if (success) {
152127
return c.json({ message: "User profile deleted successfully" }, 200);
153128
} else {
154-
// This case should ideally be handled by NotFoundError thrown from the service
155129
return c.json({ error: "Failed to delete user profile" }, 500);
156130
}
157131
} catch (error: any) {

0 commit comments

Comments
 (0)