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
19 changes: 18 additions & 1 deletion packages/backend/src/lib/routeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { Context } from "hono";
interface UserWithSensitiveData {
passwordHash?: string | null;
privateKey?: string | null;
[key: string]: unknown;
}

/**
Expand All @@ -41,6 +40,24 @@ export function sanitizeUser<T extends UserWithSensitiveData>(user: T): Sanitize
return userData as SanitizedUser<T>;
}

/**
* Removes credentials and private fields (passwordHash, privateKey, email)
* from a user object so it can be returned to anyone.
*
* Use this for every response that may contain a user other than the
* authenticated caller. Repositories return full rows (including the
* ActivityPub private key), so omitting a field here leaks it publicly.
*
* @param user - User object potentially containing sensitive data
* @returns User object safe for public responses
*/
export function toPublicUser<
T extends { passwordHash?: unknown; privateKey?: unknown; email?: unknown },
>(user: T): Omit<T, "passwordHash" | "privateKey" | "email"> {
const { passwordHash: _p, privateKey: _pk, email: _e, ...publicUser } = user;
return publicUser;
}

/**
* Pagination parameters extracted from query string
*/
Expand Down
9 changes: 5 additions & 4 deletions packages/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PasskeyService } from "../services/PasskeyService.js";
import { requireAuth } from "../middleware/auth.js";
import { rateLimit, RateLimitPresets } from "../middleware/rateLimit.js";
import { logger } from "../lib/logger.js";
import { toPublicUser } from "../lib/routeUtils.js";
import type { Context } from "hono";

const app = new Hono();
Expand Down Expand Up @@ -182,7 +183,7 @@ app.post("/register", rateLimit(RateLimitPresets.register), async (c) => {
}

// Remove sensitive data from response
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json(
{
Expand Down Expand Up @@ -263,7 +264,7 @@ app.post("/session", rateLimit(RateLimitPresets.login), async (c) => {
);

// パスワードハッシュとメールアドレスを除外してレスポンス
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json({
user: publicUser,
Expand Down Expand Up @@ -354,7 +355,7 @@ app.get("/session", requireAuth(), async (c) => {
}

// パスワードハッシュとメールアドレスを除外
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json({
user: publicUser,
Expand Down Expand Up @@ -499,7 +500,7 @@ app.post("/passkey/authenticate/finish", rateLimit(RateLimitPresets.login), asyn
const { user, session } = await passkeyService.verifyAuthentication(body.credential);

// Remove sensitive data from response
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json({
user: publicUser,
Expand Down
17 changes: 15 additions & 2 deletions packages/backend/src/routes/following.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

import { Hono } from "hono";
import type { Context } from "hono";
import type { Follow, User } from "shared";
import { requireAuth } from "../middleware/auth.js";
import { userRateLimit, RateLimitPresets } from "../middleware/rateLimit.js";
import { FollowService } from "../services/FollowService.js";
import { toPublicUser } from "../lib/routeUtils.js";

const following = new Hono();

Expand Down Expand Up @@ -157,7 +159,13 @@ following.get("/users/followers", async (c: Context) => {

try {
const followers = await followService.getFollowers(userId, limit, offset);
return c.json(followers);
// Joined user rows are full DB rows (incl. passwordHash / privateKey)
return c.json(
(followers as Array<Follow & { follower?: User | null }>).map((f) => ({
...f,
follower: f.follower ? toPublicUser(f.follower) : f.follower,
})),
);
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to get followers";
return c.json({ error: message }, 400);
Expand Down Expand Up @@ -190,7 +198,12 @@ following.get("/users/following", async (c: Context) => {

try {
const following = await followService.getFollowing(userId, limit, offset);
return c.json(following);
return c.json(
(following as Array<Follow & { followee?: User | null }>).map((f) => ({
...f,
followee: f.followee ? toPublicUser(f.followee) : f.followee,
})),
);
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to get following";
return c.json({ error: message }, 400);
Expand Down
24 changes: 8 additions & 16 deletions packages/backend/src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Hono } from "hono";
import { AuthService } from "../services/AuthService.js";
import { UserService } from "../services/UserService.js";
import { requireAuth, optionalAuth } from "../middleware/auth.js";
import { errorResponse } from "../lib/routeUtils.js";
import { errorResponse, sanitizeUser, toPublicUser } from "../lib/routeUtils.js";
import { verifyPassword } from "../utils/password.js";

const app = new Hono();
Expand Down Expand Up @@ -102,7 +102,7 @@ app.post("/", async (c) => {
});

// パスワードハッシュを除外してレスポンス
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json(
{
Expand Down Expand Up @@ -156,7 +156,7 @@ app.get("/@me", requireAuth(), async (c) => {
}

// メールアドレスは含めるが、パスワードハッシュは除外
const { passwordHash: _passwordHash, ...userData } = user;
const userData = sanitizeUser(user);

return c.json(userData);
});
Expand Down Expand Up @@ -222,15 +222,7 @@ app.get("/search", async (c) => {
});

// Remove sensitive fields
const publicUsers = searchResults.map((user) => {
const {
passwordHash: _passwordHash,
email: _email,
privateKey: _privateKey,
...publicUser
} = user;
return publicUser;
});
const publicUsers = searchResults.map((user) => toPublicUser(user));

return c.json({ users: publicUsers });
});
Expand Down Expand Up @@ -286,7 +278,7 @@ app.get("/show", optionalAuth(), async (c) => {
}

// パスワードハッシュとメールアドレスを除外
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

// Check if current user is following this user (if authenticated)
let isFollowed = false;
Expand Down Expand Up @@ -356,7 +348,7 @@ app.get("/resolve", optionalAuth(), async (c) => {
const user = await remoteActorService.resolveActorByAcct(acct);

// パスワードハッシュとメールアドレスを除外
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

// Check if current user is following this user (if authenticated)
let isFollowed = false;
Expand Down Expand Up @@ -468,7 +460,7 @@ app.get("/:id", async (c) => {
}

// パスワードハッシュとメールアドレスを除外
const { passwordHash: _passwordHash, email: _email, ...publicUser } = user;
const publicUser = toPublicUser(user);

return c.json(publicUser);
});
Expand Down Expand Up @@ -596,7 +588,7 @@ app.patch("/@me", requireAuth(), async (c) => {

try {
const updatedUser = await userService.updateProfile(user.id, updateData);
const { passwordHash: _passwordHash, ...userData } = updatedUser;
const userData = sanitizeUser(updatedUser);
return c.json(userData);
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to update profile";
Expand Down
Loading