Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"near-api-js": "^5.1.1",
"near-sign-verify": "^0.3.6",
"ora": "^8.1.1",
"pg": "^8.15.6",
"pinata-web3": "^0.5.4",
Expand Down
9 changes: 5 additions & 4 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { secureHeaders } from "hono/secure-headers";
import { db } from "./db";
import { apiRoutes } from "./routes/api";
import { AppInstance, Env } from "./types/app";
import { web3AuthJwtMiddleware } from "./utils/auth";
import { getAllowedOrigins } from "./utils/config";
import { errorHandler } from "./utils/error";
import { ServiceProvider } from "./utils/service-provider";
import { logger } from "utils/logger";
import { createAuthMiddleware } from "./middlewares/auth.middleware";

const ALLOWED_ORIGINS = getAllowedOrigins();

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

app.onError((err, c) => {
return errorHandler(err, c);
return errorHandler(err, c, logger);
});

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

// Authentication middleware
app.use("*", web3AuthJwtMiddleware);
// Apply auth middleware to all /api routes
app.use("/api/*", createAuthMiddleware());

// Mount API routes
app.route("/api", apiRoutes);
Expand Down
54 changes: 54 additions & 0 deletions apps/api/src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Context, MiddlewareHandler, Next } from "hono";
import { verify } from "near-sign-verify";

export function createAuthMiddleware(): MiddlewareHandler {
return async (c: Context, next: Next) => {
const method = c.req.method;
let accountId: string | null = null;

if (method === "GET") {
const nearAccountHeader = c.req.header("X-Near-Account");
if (
nearAccountHeader &&
nearAccountHeader.toLowerCase() !== "anonymous"
) {
accountId = nearAccountHeader;
}
// If header is missing or "anonymous", accountId remains null
c.set("accountId", accountId);
await next();
return;
}

// For non-GET requests (POST, PUT, DELETE, PATCH, etc.)
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
c.status(401);
return c.json({
error: "Unauthorized",
details: "Missing or malformed Authorization header.",
});
}

const token = authHeader.substring(7); // Remove "Bearer "

try {
const verificationResult = await verify(token, {
expectedRecipient: "curatefun.near",
requireFullAccessKey: false,
nonceMaxAge: 300000, // 5 mins
});

accountId = verificationResult.accountId;
c.set("accountId", accountId);
await next();
} catch (error) {
console.error("Token verification error:", error);
c.status(401);
return c.json({
error: "Unauthorized",
details: "Invalid token signature or recipient.",
});
}
};
}
94 changes: 88 additions & 6 deletions apps/api/src/routes/api/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,49 @@ feedsRoutes.get("/", async (c) => {
* Create a new feed
*/
feedsRoutes.post("/", async (c) => {
const accountId = c.get("accountId");
if (!accountId) {
return c.json(
{ error: "Unauthorized. User must be logged in to create a feed." },
401,
);
}

const body = await c.req.json();
const validationResult = insertFeedSchema.safeParse(body);
const partialValidationResult = insertFeedSchema
.omit({ created_by: true })
.safeParse(body);

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

const feedDataWithCreator = {
...partialValidationResult.data,
created_by: accountId,
};

const finalValidationResult = insertFeedSchema.safeParse(feedDataWithCreator);
if (!finalValidationResult.success) {
logger.error(
"Error in final validation after adding created_by",
finalValidationResult.error,
);
return badRequest(
c,
"Internal validation error",
finalValidationResult.error.flatten(),
);
}

const sp = c.get("sp");
const feedService = sp.getFeedService();
try {
const newFeed = await feedService.createFeed(validationResult.data);
const newFeed = await feedService.createFeed(finalValidationResult.data);
return c.json(newFeed, 201);
} catch (error) {
logger.error("Error creating feed:", error);
Expand Down Expand Up @@ -66,16 +98,37 @@ feedsRoutes.get("/:feedId", async (c) => {
* Update an existing feed
*/
feedsRoutes.put("/:feedId", async (c) => {
const accountId = c.get("accountId");
if (!accountId) {
return c.json(
{ error: "Unauthorized. User must be logged in to update a feed." },
401,
);
}

const feedId = c.req.param("feedId");
const sp = c.get("sp");
const feedService = sp.getFeedService();

const canUpdate = await feedService.hasPermission(
accountId,
feedId,
"update",
);
if (!canUpdate) {
return c.json(
{ error: "Forbidden. You do not have permission to update this feed." },
403,
);
}

const body = await c.req.json();
const validationResult = updateFeedSchema.safeParse(body);

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

const sp = c.get("sp");
const feedService = sp.getFeedService();
try {
const updatedFeed = await feedService.updateFeed(
feedId,
Expand All @@ -97,6 +150,14 @@ feedsRoutes.put("/:feedId", async (c) => {
* Example: /api/feeds/solana/process?distributors=@curatedotfun/rss
*/
feedsRoutes.post("/:feedId/process", async (c) => {
const accountId = c.get("accountId");
if (!accountId) {
return c.json(
{ error: "Unauthorized. User must be logged in to process a feed." },
401,
);
}

const sp = c.get("sp");
const feedService = sp.getFeedService();

Expand Down Expand Up @@ -127,9 +188,30 @@ feedsRoutes.post("/:feedId/process", async (c) => {
* Delete a specific feed by its ID
*/
feedsRoutes.delete("/:feedId", async (c) => {
const accountId = c.get("accountId");
if (!accountId) {
return c.json(
{ error: "Unauthorized. User must be logged in to delete a feed." },
401,
);
}

const feedId = c.req.param("feedId");
const sp = c.get("sp");
const feedService = sp.getFeedService();

const canDelete = await feedService.hasPermission(
accountId,
feedId,
"delete",
);
if (!canDelete) {
return c.json(
{ error: "Forbidden. You do not have permission to delete this feed." },
403,
);
}

try {
const result = await feedService.deleteFeed(feedId);
if (!result) {
Expand Down
64 changes: 19 additions & 45 deletions apps/api/src/routes/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { InsertUserData } from "../../services/users.service";
import { Env } from "../../types/app";
import {
NearAccountError,
NotFoundError,
UserServiceError,
} from "../../types/errors";
import { ServiceProvider } from "../../utils/service-provider";
import {
insertUserSchema,
updateUserSchema,
} from "../../validation/users.validation";
import { insertUserSchema, updateUserSchema } from "@curatedotfun/shared-db";

const usersRoutes = new Hono<Env>();

// --- GET /api/users/me ---
usersRoutes.get("/me", async (c) => {
const jwtPayload = c.get("jwtPayload");
const authProviderId = jwtPayload?.authProviderId;
const accountId = c.get("accountId");

if (!authProviderId) {
return c.json(
{ error: "Unauthorized: Missing or invalid authentication token" },
401,
);
if (!accountId) {
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
}

try {
const userService = ServiceProvider.getInstance().getUserService();
const user = await userService.findUserByAuthProviderId(authProviderId);
const user = await userService.findUserByNearAccountId(accountId);

if (!user) {
return c.json({ error: "User profile not found" }, 404);
return c.json(
{ error: "User profile not found for the given NEAR account ID." },
404,
);
}

return c.json({ profile: user });
Expand All @@ -46,23 +41,11 @@ usersRoutes.get("/me", async (c) => {
// --- POST /api/users ---
usersRoutes.post("/", zValidator("json", insertUserSchema), async (c) => {
const createUserData = c.req.valid("json");
const jwtPayload = c.get("jwtPayload");
const authProviderId = jwtPayload?.authProviderId;

if (!authProviderId) {
return c.json(
{ error: "Unauthorized: Missing or invalid authentication token" },
401,
);
}

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

const newUser = await userService.createUser({
auth_provider_id: authProviderId,
...createUserData,
} as InsertUserData);
const newUser = await userService.createUser(createUserData);

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

if (!authProviderId) {
return c.json(
{ error: "Unauthorized: Missing or invalid authentication token" },
401,
);
if (!accountId) {
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
}

try {
const userService = ServiceProvider.getInstance().getUserService();
const updatedUser = await userService.updateUser(
authProviderId,
const updatedUser = await userService.updateUserByNearAccountId(
accountId,
updateData,
);

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

// --- DELETE /api/users/me ---
usersRoutes.delete("/me", async (c) => {
const jwtPayload = c.get("jwtPayload");
const authProviderId = jwtPayload?.authProviderId;
const accountId = c.get("accountId");

if (!authProviderId) {
return c.json(
{ error: "Unauthorized: Missing or invalid authentication token" },
401,
);
if (!accountId) {
return c.json({ error: "Unauthorized: User not authenticated." }, 401);
}

try {
const userService = ServiceProvider.getInstance().getUserService();
const success = await userService.deleteUser(authProviderId);
const success = await userService.deleteUserByNearAccountId(accountId);

if (success) {
return c.json({ message: "User profile deleted successfully" }, 200);
} else {
// This case should ideally be handled by NotFoundError thrown from the service
return c.json({ error: "Failed to delete user profile" }, 500);
}
} catch (error: any) {
Expand Down
Loading
Loading