refactor(api): ♻️ Throw ApiError instead of returning error JSON

This commit is contained in:
Jesse Wierzbinski 2024-12-30 18:00:23 +01:00
parent c14621ee06
commit fbfd237f27
No known key found for this signature in database
88 changed files with 458 additions and 483 deletions

View file

@ -7,6 +7,7 @@ import type { Context } from "hono";
import { setCookie } from "hono/cookie"; import { setCookie } from "hono/cookie";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -203,7 +204,7 @@ export default apiRoute((app) =>
const application = await Application.fromClientId(client_id); const application = await Application.fromClientId(client_id);
if (!application) { if (!application) {
return context.json({ error: "Invalid application" }, 400); throw new ApiError(400, "Invalid application");
} }
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -4,6 +4,7 @@ import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -91,13 +92,13 @@ export default apiRoute((app) =>
const { reblogs, notify, languages } = context.req.valid("json"); const { reblogs, notify, languages } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
let relationship = await Relationship.fromOwnerAndSubject( let relationship = await Relationship.fromOwnerAndSubject(

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -82,7 +83,7 @@ export default apiRoute((app) =>
// TODO: Add follower/following privacy settings // TODO: Add follower/following privacy settings
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const { objects, link } = await Timeline.getUserTimeline( const { objects, link } = await Timeline.getUserTimeline(

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -81,7 +82,7 @@ export default apiRoute((app) =>
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
// TODO: Add follower/following privacy settings // TODO: Add follower/following privacy settings

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -63,7 +64,7 @@ export default apiRoute((app) =>
const foundUser = await User.fromId(id); const foundUser = await User.fromId(id);
if (!foundUser) { if (!foundUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
return context.json(foundUser.toApi(user?.id === foundUser.id), 200); return context.json(foundUser.toApi(user?.id === foundUser.id), 200);

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -90,13 +91,13 @@ export default apiRoute((app) =>
const { notifications } = context.req.valid("json"); const { notifications } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -83,13 +84,13 @@ export default apiRoute((app) =>
const { comment } = context.req.valid("json"); const { comment } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -77,17 +78,17 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
if (otherUser.isLocal()) { if (otherUser.isLocal()) {
return context.json({ error: "Cannot refetch a local user" }, 400); throw new ApiError(400, "Cannot refetch a local user");
} }
const newUser = await otherUser.updateFromRemote(); const newUser = await otherUser.updateFromRemote();

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const oppositeRelationship = await Relationship.fromOwnerAndSubject( const oppositeRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -116,7 +116,9 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: `Cannot assign role 'higherPriorityRole' with priority 3 to user: your highest role priority is 2`, error: "Forbidden",
details:
"User with highest role priority 2 cannot assign role with priority 3",
}); });
}); });
@ -156,7 +158,9 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: `Cannot remove role 'higherPriorityRole' with priority 3 from user: your highest role priority is 2`, error: "Forbidden",
details:
"User with highest role priority 2 cannot remove role with priority 3",
}); });
await higherPriorityRole.unlinkUser(users[1].id); await higherPriorityRole.unlinkUser(users[1].id);

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Role, User } from "@versia/kit/db"; import { Role, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -114,18 +115,18 @@ export default apiRoute((app) => {
const { id, role_id } = context.req.valid("param"); const { id, role_id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const targetUser = await User.fromId(id); const targetUser = await User.fromId(id);
const role = await Role.fromId(role_id); const role = await Role.fromId(role_id);
if (!role) { if (!role) {
return context.json({ error: "Role not found" }, 404); throw new ApiError(404, "Role not found");
} }
if (!targetUser) { if (!targetUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
// Priority check // Priority check
@ -136,11 +137,10 @@ export default apiRoute((app) => {
); );
if (role.data.priority > userHighestRole.data.priority) { if (role.data.priority > userHighestRole.data.priority) {
return context.json( throw new ApiError(
{
error: `Cannot assign role '${role.data.name}' with priority ${role.data.priority} to user: your highest role priority is ${userHighestRole.data.priority}`,
},
403, 403,
"Forbidden",
`User with highest role priority ${userHighestRole.data.priority} cannot assign role with priority ${role.data.priority}`,
); );
} }
@ -154,18 +154,18 @@ export default apiRoute((app) => {
const { id, role_id } = context.req.valid("param"); const { id, role_id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const targetUser = await User.fromId(id); const targetUser = await User.fromId(id);
const role = await Role.fromId(role_id); const role = await Role.fromId(role_id);
if (!role) { if (!role) {
return context.json({ error: "Role not found" }, 404); throw new ApiError(404, "Role not found");
} }
if (!targetUser) { if (!targetUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
// Priority check // Priority check
@ -176,11 +176,10 @@ export default apiRoute((app) => {
); );
if (role.data.priority > userHighestRole.data.priority) { if (role.data.priority > userHighestRole.data.priority) {
return context.json( throw new ApiError(
{
error: `Cannot remove role '${role.data.name}' with priority ${role.data.priority} from user: your highest role priority is ${userHighestRole.data.priority}`,
},
403, 403,
"Forbidden",
`User with highest role priority ${userHighestRole.data.priority} cannot remove role with priority ${role.data.priority}`,
); );
} }

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { Role, User } from "@versia/kit/db"; import { Role, User } from "@versia/kit/db";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -59,7 +60,7 @@ export default apiRoute((app) => {
const targetUser = await User.fromId(id); const targetUser = await User.fromId(id);
if (!targetUser) { if (!targetUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const roles = await Role.getUserRoles( const roles = await Role.getUserRoles(

View file

@ -4,6 +4,7 @@ import { Note, Timeline, User } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -96,7 +97,7 @@ export default apiRoute((app) =>
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const { const {

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -80,13 +81,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(
@ -94,9 +95,7 @@ export default apiRoute((app) =>
otherUser, otherUser,
); );
if (!(await self.unfollow(otherUser, foundRelationship))) { await self.unfollow(otherUser, foundRelationship);
return context.json({ error: "Failed to unfollow user" }, 500);
}
return context.json(foundRelationship.toApi(), 200); return context.json(foundRelationship.toApi(), 200);
}), }),

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const user = await User.fromId(id); const user = await User.fromId(id);
if (!user) { if (!user) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) { if (!otherUser) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
const foundRelationship = await Relationship.fromOwnerAndSubject( const foundRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -4,6 +4,7 @@ import { User, db } from "@versia/kit/db";
import { RolePermissions, type Users } from "@versia/kit/tables"; import { RolePermissions, type Users } from "@versia/kit/tables";
import { type InferSelectModel, sql } from "drizzle-orm"; import { type InferSelectModel, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -73,7 +74,7 @@ export default apiRoute((app) =>
const { id: ids } = context.req.valid("query"); const { id: ids } = context.req.valid("query");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
// Find followers of the accounts in "ids", that you also follow // Find followers of the accounts in "ids", that you also follow

View file

@ -4,6 +4,7 @@ import { User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -65,7 +66,7 @@ export default apiRoute((app) =>
); );
if (!user) { if (!user) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
return context.json(user.toApi(), 200); return context.json(user.toApi(), 200);

View file

@ -6,6 +6,7 @@ import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -143,12 +144,7 @@ export default apiRoute((app) =>
context.req.valid("json"); context.req.valid("json");
if (!config.signups.registration) { if (!config.signups.registration) {
return context.json( throw new ApiError(422, "Registration is disabled");
{
error: "Registration is disabled",
},
422,
);
} }
const errors: { const errors: {
@ -318,16 +314,14 @@ export default apiRoute((app) =>
.join(", ")}`, .join(", ")}`,
) )
.join(", "); .join(", ");
return context.json( throw new ApiError(
{ 422,
error: `Validation failed: ${errorsText}`, `Validation failed: ${errorsText}`,
details: Object.fromEntries( Object.fromEntries(
Object.entries(errors.details).filter( Object.entries(errors.details).filter(
([_, errors]) => errors.length > 0, ([_, errors]) => errors.length > 0,
), ),
), ),
},
422,
); );
} }

View file

@ -4,6 +4,7 @@ import { Instance, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -118,7 +119,7 @@ export default apiRoute((app) =>
const uri = await User.webFinger(manager, username, domain); const uri = await User.webFinger(manager, username, domain);
if (!uri) { if (!uri) {
return context.json({ error: "Account not found" }, 404); throw new ApiError(404, "Account not found");
} }
const foundAccount = await User.resolve(uri); const foundAccount = await User.resolve(uri);
@ -127,6 +128,6 @@ export default apiRoute((app) =>
return context.json(foundAccount.toApi(), 200); return context.json(foundAccount.toApi(), 200);
} }
return context.json({ error: "Account not found" }, 404); throw new ApiError(404, "Account not found");
}), }),
); );

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship } from "@versia/kit/db"; import { Relationship } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -63,7 +64,7 @@ export default apiRoute((app) =>
const ids = Array.isArray(id) ? id : [id]; const ids = Array.isArray(id) ? id : [id];
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const relationships = await Relationship.fromOwnerAndSubjects( const relationships = await Relationship.fromOwnerAndSubjects(

View file

@ -11,6 +11,7 @@ import { RolePermissions, Users } from "@versia/kit/tables";
import { eq, ilike, not, or, sql } from "drizzle-orm"; import { eq, ilike, not, or, sql } from "drizzle-orm";
import stringComparison from "string-comparison"; import stringComparison from "string-comparison";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -80,7 +81,7 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self && following) { if (!self && following) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { username, domain } = parseUserAddress(q); const { username, domain } = parseUserAddress(q);

View file

@ -8,6 +8,7 @@ import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { contentToHtml } from "~/classes/functions/status"; import { contentToHtml } from "~/classes/functions/status";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
@ -213,7 +214,7 @@ export default apiRoute((app) =>
} = context.req.valid("json"); } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const self = user.data; const self = user.data;
@ -257,10 +258,7 @@ export default apiRoute((app) =>
); );
if (existingUser) { if (existingUser) {
return context.json( throw new ApiError(422, "Username is already taken");
{ error: "Username is already taken" },
422,
);
} }
self.username = username; self.username = username;
@ -402,7 +400,7 @@ export default apiRoute((app) =>
const output = await User.fromId(self.id); const output = await User.fromId(self.id);
if (!output) { if (!output) {
return context.json({ error: "Couldn't edit user" }, 500); throw new ApiError(500, "Couldn't edit user");
} }
return context.json(output.toApi(), 200); return context.json(output.toApi(), 200);

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -47,7 +48,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
return context.json(user.toApi(true), 200); return context.json(user.toApi(true), 200);

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { Application } from "@versia/kit/db"; import { Application } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -49,10 +50,10 @@ export default apiRoute((app) =>
const { user, token } = context.get("auth"); const { user, token } = context.get("auth");
if (!token) { if (!token) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const application = await Application.getFromToken( const application = await Application.getFromToken(
@ -60,7 +61,7 @@ export default apiRoute((app) =>
); );
if (!application) { if (!application) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
return context.json( return context.json(

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -66,7 +67,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { objects: blocks, link } = await Timeline.getUserTimeline( const { objects: blocks, link } = await Timeline.getUserTimeline(

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { generateChallenge } from "@/challenges"; import { generateChallenge } from "@/challenges";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -54,10 +55,7 @@ const route = createRoute({
export default apiRoute((app) => export default apiRoute((app) =>
app.openapi(route, async (context) => { app.openapi(route, async (context) => {
if (!config.validation.challenges.enabled) { if (!config.validation.challenges.enabled) {
return context.json( throw new ApiError(400, "Challenges are disabled in config");
{ error: "Challenges are disabled in config" },
400,
);
} }
const result = await generateChallenge(); const result = await generateChallenge();

View file

@ -5,6 +5,7 @@ import { Attachment, Emoji, db } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables"; import { Emojis, RolePermissions } from "@versia/kit/tables";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -212,13 +213,13 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const emoji = await Emoji.fromId(id); const emoji = await Emoji.fromId(id);
if (!emoji) { if (!emoji) {
return context.json({ error: "Emoji not found" }, 404); throw new ApiError(404, "Emoji not found");
} }
// Check if user is admin // Check if user is admin
@ -226,11 +227,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) && !user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id emoji.data.ownerId !== user.data.id
) { ) {
return context.json( throw new ApiError(
{
error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403, 403,
"Cannot modify emoji not owned by you",
`This emoji is either global (and you do not have the '${RolePermissions.ManageEmojis}' permission) or not owned by you`,
); );
} }
@ -242,13 +242,13 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const emoji = await Emoji.fromId(id); const emoji = await Emoji.fromId(id);
if (!emoji) { if (!emoji) {
return context.json({ error: "Emoji not found" }, 404); throw new ApiError(404, "Emoji not found");
} }
// Check if user is admin // Check if user is admin
@ -256,11 +256,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) && !user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id emoji.data.ownerId !== user.data.id
) { ) {
return context.json( throw new ApiError(
{
error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403, 403,
"Cannot modify emoji not owned by you",
`This emoji is either global (and you do not have the '${RolePermissions.ManageEmojis}' permission) or not owned by you`,
); );
} }
@ -275,11 +274,10 @@ export default apiRoute((app) => {
} = context.req.valid("json"); } = context.req.valid("json");
if (!user.hasPermission(RolePermissions.ManageEmojis) && emojiGlobal) { if (!user.hasPermission(RolePermissions.ManageEmojis) && emojiGlobal) {
return context.json( throw new ApiError(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can make an emoji global or not`,
},
401, 401,
"Missing permissions",
`'${RolePermissions.ManageEmojis}' permission is needed to upload global emojis`,
); );
} }
@ -293,11 +291,10 @@ export default apiRoute((app) => {
: await mimeLookup(element); : await mimeLookup(element);
if (!contentType.startsWith("image/")) { if (!contentType.startsWith("image/")) {
return context.json( throw new ApiError(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
422, 422,
"Invalid content type",
`Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
); );
} }
@ -334,13 +331,13 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const emoji = await Emoji.fromId(id); const emoji = await Emoji.fromId(id);
if (!emoji) { if (!emoji) {
return context.json({ error: "Emoji not found" }, 404); throw new ApiError(404, "Emoji not found");
} }
// Check if user is admin // Check if user is admin
@ -348,11 +345,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) && !user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id emoji.data.ownerId !== user.data.id
) { ) {
return context.json( throw new ApiError(
{
error: `You cannot delete this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
},
403, 403,
"Cannot delete emoji not owned by you",
`This emoji is either global (and you do not have the '${RolePermissions.ManageEmojis}' permission) or not owned by you`,
); );
} }

View file

@ -5,6 +5,7 @@ import { Attachment, Emoji } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables"; import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm"; import { and, eq, isNull, or } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -117,15 +118,14 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
if (!user.hasPermission(RolePermissions.ManageEmojis) && global) { if (!user.hasPermission(RolePermissions.ManageEmojis) && global) {
return context.json( throw new ApiError(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
},
401, 401,
"Missing permissions",
`Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
); );
} }
@ -139,11 +139,10 @@ export default apiRoute((app) =>
); );
if (existing) { if (existing) {
return context.json( throw new ApiError(
{
error: `An emoji with the shortcode ${shortcode} already exists, either owned by you or global.`,
},
422, 422,
"Emoji already exists",
`An emoji with the shortcode ${shortcode} already exists, either owned by you or global.`,
); );
} }
@ -154,11 +153,10 @@ export default apiRoute((app) =>
element instanceof File ? element.type : await mimeLookup(element); element instanceof File ? element.type : await mimeLookup(element);
if (!contentType.startsWith("image/")) { if (!contentType.startsWith("image/")) {
return context.json( throw new ApiError(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
422, 422,
"Invalid content type",
`Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
); );
} }

View file

@ -4,6 +4,7 @@ import { Note, Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { objects: favourites, link } = await Timeline.getNoteTimeline( const { objects: favourites, link } = await Timeline.getNoteTimeline(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -66,7 +67,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { account_id } = context.req.valid("param"); const { account_id } = context.req.valid("param");
@ -74,7 +75,7 @@ export default apiRoute((app) =>
const account = await User.fromId(account_id); const account = await User.fromId(account_id);
if (!account) { if (!account) {
return context.json({ error: "Account not found" }, 404); throw new ApiError(404, "Account not found");
} }
const oppositeRelationship = await Relationship.fromOwnerAndSubject( const oppositeRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db"; import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -66,7 +67,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { account_id } = context.req.valid("param"); const { account_id } = context.req.valid("param");
@ -74,7 +75,7 @@ export default apiRoute((app) =>
const account = await User.fromId(account_id); const account = await User.fromId(account_id);
if (!account) { if (!account) {
return context.json({ error: "Account not found" }, 404); throw new ApiError(404, "Account not found");
} }
const oppositeRelationship = await Relationship.fromOwnerAndSubject( const oppositeRelationship = await Relationship.fromOwnerAndSubject(

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { objects: followRequests, link } = const { objects: followRequests, link } =

View file

@ -5,6 +5,7 @@ import { db } from "@versia/kit/db";
import { Markers, RolePermissions } from "@versia/kit/tables"; import { Markers, RolePermissions } from "@versia/kit/tables";
import { type SQL, and, eq } from "drizzle-orm"; import { type SQL, and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -119,7 +120,7 @@ export default apiRoute((app) => {
const timeline = Array.isArray(timelines) ? timelines : []; const timeline = Array.isArray(timelines) ? timelines : [];
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
if (!timeline) { if (!timeline) {
@ -191,7 +192,7 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const markers: ApiMarker = { const markers: ApiMarker = {

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db"; import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -122,7 +123,7 @@ export default apiRoute((app) => {
const attachment = await Attachment.fromId(id); const attachment = await Attachment.fromId(id);
if (!attachment) { if (!attachment) {
return context.json({ error: "Media not found" }, 404); throw new ApiError(404, "Media not found");
} }
const { description, thumbnail } = context.req.valid("form"); const { description, thumbnail } = context.req.valid("form");
@ -159,7 +160,7 @@ export default apiRoute((app) => {
const attachment = await Attachment.fromId(id); const attachment = await Attachment.fromId(id);
if (!attachment) { if (!attachment) {
return context.json({ error: "Media not found" }, 404); throw new ApiError(404, "Media not found");
} }
return context.json(attachment.toApi(), 200); return context.json(attachment.toApi(), 200);

View file

@ -4,6 +4,7 @@ import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -90,11 +91,9 @@ export default apiRoute((app) =>
const { file, thumbnail, description } = context.req.valid("form"); const { file, thumbnail, description } = context.req.valid("form");
if (file.size > config.validation.max_media_size) { if (file.size > config.validation.max_media_size) {
return context.json( throw new ApiError(
{
error: `File too large, max size is ${config.validation.max_media_size} bytes`,
},
413, 413,
`File too large, max size is ${config.validation.max_media_size} bytes`,
); );
} }
@ -102,7 +101,11 @@ export default apiRoute((app) =>
config.validation.enforce_mime_types && config.validation.enforce_mime_types &&
!config.validation.allowed_mime_types.includes(file.type) !config.validation.allowed_mime_types.includes(file.type)
) { ) {
return context.json({ error: "Disallowed file type" }, 415); throw new ApiError(
415,
`File type ${file.type} is not allowed`,
`Allowed types: ${config.validation.allowed_mime_types.join(", ")}`,
);
} }
const sha256 = new Bun.SHA256(); const sha256 = new Bun.SHA256();

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { objects: mutes, link } = await Timeline.getUserTimeline( const { objects: mutes, link } = await Timeline.getUserTimeline(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Notification } from "@versia/kit/db"; import { Notification } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -55,13 +56,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const notification = await Notification.fromId(id); const notification = await Notification.fromId(id);
if (!notification) { if (!notification) {
return context.json({ error: "Notification not found" }, 404); throw new ApiError(404, "Notification not found");
} }
await notification.update({ await notification.update({

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Notification } from "@versia/kit/db"; import { Notification } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -64,13 +65,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const notification = await Notification.fromId(id, user.id); const notification = await Notification.fromId(id, user.id);
if (!notification) { if (!notification) {
return context.json({ error: "Notification not found" }, 404); throw new ApiError(404, "Notification not found");
} }
return context.json(await notification.toApi(), 200); return context.json(await notification.toApi(), 200);

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -42,7 +43,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => { app.openapi(route, async (context) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await user.clearAllNotifications(); await user.clearAllNotifications();

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -53,7 +54,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { "ids[]": ids } = context.req.valid("query"); const { "ids[]": ids } = context.req.valid("query");

View file

@ -4,6 +4,7 @@ import { Notification, Timeline } from "@versia/kit/db";
import { Notifications, RolePermissions } from "@versia/kit/tables"; import { Notifications, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -121,7 +122,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => { app.openapi(route, async (context) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { const {

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -48,7 +49,7 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await self.update({ await self.update({

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -48,7 +49,7 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await self.update({ await self.update({

View file

@ -144,7 +144,9 @@ describe("/api/v1/roles/:id", () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: `Cannot edit role 'higherPriorityRole' with priority 3: your highest role priority is 2`, error: "Forbidden",
details:
"User with highest role priority 2 cannot edit role with priority 3",
}); });
}); });
@ -163,7 +165,8 @@ describe("/api/v1/roles/:id", () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: "You cannot add or remove the following permissions you do not yourself have: impersonate", error: "Forbidden",
details: "User cannot add or remove permissions they do not have",
}); });
}); });
@ -226,7 +229,9 @@ describe("/api/v1/roles/:id", () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: `Cannot delete role 'higherPriorityRole' with priority 3: your highest role priority is 2`, error: "Forbidden",
details:
"User with highest role priority 2 cannot delete role with priority 3",
}); });
}); });
}); });

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -156,13 +157,13 @@ export default apiRoute((app) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const role = await Role.fromId(id); const role = await Role.fromId(id);
if (!role) { if (!role) {
return context.json({ error: "Role not found" }, 404); throw new ApiError(404, "Role not found");
} }
return context.json(role.toApi(), 200); return context.json(role.toApi(), 200);
@ -175,13 +176,13 @@ export default apiRoute((app) => {
context.req.valid("json"); context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const role = await Role.fromId(id); const role = await Role.fromId(id);
if (!role) { if (!role) {
return context.json({ error: "Role not found" }, 404); throw new ApiError(404, "Role not found");
} }
// Priority check // Priority check
@ -192,11 +193,10 @@ export default apiRoute((app) => {
); );
if (role.data.priority > userHighestRole.data.priority) { if (role.data.priority > userHighestRole.data.priority) {
return context.json( throw new ApiError(
{
error: `Cannot edit role '${role.data.name}' with priority ${role.data.priority}: your highest role priority is ${userHighestRole.data.priority}`,
},
403, 403,
"Forbidden",
`User with highest role priority ${userHighestRole.data.priority} cannot edit role with priority ${role.data.priority}`,
); );
} }
@ -208,11 +208,10 @@ export default apiRoute((app) => {
).every((p) => userPermissions.includes(p)); ).every((p) => userPermissions.includes(p));
if (!hasPermissions) { if (!hasPermissions) {
return context.json( throw new ApiError(
{
error: `You cannot add or remove the following permissions you do not yourself have: ${permissions.join(", ")}`,
},
403, 403,
"Forbidden",
"User cannot add or remove permissions they do not have",
); );
} }
} }
@ -234,13 +233,13 @@ export default apiRoute((app) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const role = await Role.fromId(id); const role = await Role.fromId(id);
if (!role) { if (!role) {
return context.json({ error: "Role not found" }, 404); throw new ApiError(404, "Role not found");
} }
// Priority check // Priority check
@ -251,11 +250,10 @@ export default apiRoute((app) => {
); );
if (role.data.priority > userHighestRole.data.priority) { if (role.data.priority > userHighestRole.data.priority) {
return context.json( throw new ApiError(
{
error: `Cannot delete role '${role.data.name}' with priority ${role.data.priority}: your highest role priority is ${userHighestRole.data.priority}`,
},
403, 403,
"Forbidden",
`User with highest role priority ${userHighestRole.data.priority} cannot delete role with priority ${role.data.priority}`,
); );
} }

View file

@ -126,7 +126,7 @@ describe(meta.route, () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: "You cannot create a role with higher priority than your own", error: "Cannot create role with higher priority than your own",
}); });
}); });
@ -150,7 +150,8 @@ describe(meta.route, () => {
expect(response.status).toBe(403); expect(response.status).toBe(403);
const output = await response.json(); const output = await response.json();
expect(output).toMatchObject({ expect(output).toMatchObject({
error: "You cannot create a role with the following permissions you do not yourself have: impersonate", error: "Cannot create role with permissions you do not have",
details: "Forbidden permissions: impersonate",
}); });
}); });
}); });

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -96,7 +97,7 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const roles = await Role.getAll(); const roles = await Role.getAll();
@ -113,7 +114,7 @@ export default apiRoute((app) => {
context.req.valid("json"); context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
// Priority check // Priority check
@ -124,11 +125,9 @@ export default apiRoute((app) => {
); );
if (priority > userHighestRole.data.priority) { if (priority > userHighestRole.data.priority) {
return context.json( throw new ApiError(
{
error: "You cannot create a role with higher priority than your own",
},
403, 403,
"Cannot create role with higher priority than your own",
); );
} }
@ -140,11 +139,10 @@ export default apiRoute((app) => {
).every((p) => userPermissions.includes(p)); ).every((p) => userPermissions.includes(p));
if (!hasPermissions) { if (!hasPermissions) {
return context.json( throw new ApiError(
{
error: `You cannot create a role with the following permissions you do not yourself have: ${permissions.join(", ")}`,
},
403, 403,
"Cannot create role with permissions you do not have",
`Forbidden permissions: ${permissions.join(", ")}`,
); );
} }
} }

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -65,7 +66,7 @@ export default apiRoute((app) =>
const foundStatus = await Note.fromId(id, user?.id); const foundStatus = await Note.fromId(id, user?.id);
if (!foundStatus) { if (!foundStatus) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
const ancestors = await foundStatus.getAncestors(user ?? null); const ancestors = await foundStatus.getAncestors(user ?? null);

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -68,13 +69,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user?.id); const note = await Note.fromId(id, user?.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
await user.like(note); await user.like(note);

View file

@ -4,6 +4,7 @@ import { Note, Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -77,13 +78,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user?.id); const note = await Note.fromId(id, user?.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
const { objects, link } = await Timeline.getUserTimeline( const { objects, link } = await Timeline.getUserTimeline(

View file

@ -4,6 +4,7 @@ import { Attachment, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -216,7 +217,7 @@ export default apiRoute((app) => {
const note = await Note.fromId(id, user?.id); const note = await Note.fromId(id, user?.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
return context.json(await note.toApi(user), 200); return context.json(await note.toApi(user), 200);
@ -229,11 +230,11 @@ export default apiRoute((app) => {
const note = await Note.fromId(id, user?.id); const note = await Note.fromId(id, user?.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
if (note.author.id !== user?.id) { if (note.author.id !== user?.id) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
// TODO: Delete and redraft // TODO: Delete and redraft
@ -249,17 +250,17 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user?.id); const note = await Note.fromId(id, user?.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
if (note.author.id !== user.id) { if (note.author.id !== user.id) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
// TODO: Polls // TODO: Polls
@ -275,7 +276,10 @@ export default apiRoute((app) => {
media_ids.length > 0 ? await Attachment.fromIds(media_ids) : []; media_ids.length > 0 ? await Attachment.fromIds(media_ids) : [];
if (foundAttachments.length !== media_ids.length) { if (foundAttachments.length !== media_ids.length) {
return context.json({ error: "Invalid media IDs" }, 422); throw new ApiError(
422,
"Some attachments referenced by media_ids not found",
);
} }
const newNote = await note.updateFromData({ const newNote = await note.updateFromData({

View file

@ -4,6 +4,7 @@ import { Note, db } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import type { SQL } from "drizzle-orm"; import type { SQL } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -76,17 +77,17 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const foundStatus = await Note.fromId(id, user?.id); const foundStatus = await Note.fromId(id, user?.id);
if (!foundStatus) { if (!foundStatus) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
if (foundStatus.author.id !== user.id) { if (foundStatus.author.id !== user.id) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
if ( if (
@ -98,7 +99,7 @@ export default apiRoute((app) =>
), ),
}) })
) { ) {
return context.json({ error: "Already pinned" }, 422); throw new ApiError(422, "Already pinned");
} }
await user.pin(foundStatus); await user.pin(foundStatus);

View file

@ -4,6 +4,7 @@ import { Note } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -101,13 +102,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user.id); const note = await Note.fromId(id, user.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
const existingReblog = await Note.fromSql( const existingReblog = await Note.fromSql(
@ -115,7 +116,7 @@ export default apiRoute((app) =>
); );
if (existingReblog) { if (existingReblog) {
return context.json({ error: "Already reblogged" }, 422); throw new ApiError(422, "Already reblogged");
} }
const newReblog = await Note.insert({ const newReblog = await Note.insert({
@ -127,14 +128,10 @@ export default apiRoute((app) =>
applicationId: null, applicationId: null,
}); });
if (!newReblog) {
return context.json({ error: "Failed to reblog" }, 500);
}
const finalNewReblog = await Note.fromId(newReblog.id, user?.id); const finalNewReblog = await Note.fromId(newReblog.id, user?.id);
if (!finalNewReblog) { if (!finalNewReblog) {
return context.json({ error: "Failed to reblog" }, 500); throw new ApiError(500, "Failed to reblog");
} }
if (note.author.isLocal() && user.isLocal()) { if (note.author.isLocal() && user.isLocal()) {

View file

@ -4,6 +4,7 @@ import { Note, Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables"; import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -76,13 +77,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user.id); const note = await Note.fromId(id, user.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
const { objects, link } = await Timeline.getUserTimeline( const { objects, link } = await Timeline.getUserTimeline(

View file

@ -4,6 +4,7 @@ import type { StatusSource as ApiStatusSource } from "@versia/client/types";
import { Note } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user.id); const note = await Note.fromId(id, user.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
return context.json( return context.json(

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -67,13 +68,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user.id); const note = await Note.fromId(id, user.id);
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
await user.unlike(note); await user.unlike(note);

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -67,23 +68,23 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const status = await Note.fromId(id, user.id); const status = await Note.fromId(id, user.id);
if (!status) { if (!status) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
if (status.author.id !== user.id) { if (status.author.id !== user.id) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await user.unpin(status); await user.unpin(status);
if (!status) { if (!status) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
return context.json(await status.toApi(user), 200); return context.json(await status.toApi(user), 200);

View file

@ -4,6 +4,7 @@ import { Note } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -76,14 +77,14 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const note = await Note.fromId(id, user.id); const note = await Note.fromId(id, user.id);
// Check if user is authorized to view this status (if it's private) // Check if user is authorized to view this status (if it's private)
if (!(note && (await note?.isViewableByUser(user)))) { if (!(note && (await note?.isViewableByUser(user)))) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
const existingReblog = await Note.fromSql( const existingReblog = await Note.fromSql(
@ -93,7 +94,7 @@ export default apiRoute((app) =>
); );
if (!existingReblog) { if (!existingReblog) {
return context.json({ error: "Not already reblogged" }, 422); throw new ApiError(422, "Note already reblogged");
} }
await existingReblog.delete(); await existingReblog.delete();
@ -103,7 +104,7 @@ export default apiRoute((app) =>
const newNote = await Note.fromId(id, user.id); const newNote = await Note.fromId(id, user.id);
if (!newNote) { if (!newNote) {
return context.json({ error: "Record not found" }, 404); throw new ApiError(404, "Note not found");
} }
return context.json(await newNote.toApi(user), 200); return context.json(await newNote.toApi(user), 200);

View file

@ -4,6 +4,7 @@ import { Attachment, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -152,7 +153,7 @@ export default apiRoute((app) =>
const { user, application } = context.get("auth"); const { user, application } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { const {
@ -172,19 +173,22 @@ export default apiRoute((app) =>
media_ids.length > 0 ? await Attachment.fromIds(media_ids) : []; media_ids.length > 0 ? await Attachment.fromIds(media_ids) : [];
if (foundAttachments.length !== media_ids.length) { if (foundAttachments.length !== media_ids.length) {
return context.json({ error: "Invalid media IDs" }, 422); throw new ApiError(
422,
"Some attachments referenced by media_ids not found",
);
} }
// Check that in_reply_to_id and quote_id are real posts if provided // Check that in_reply_to_id and quote_id are real posts if provided
if (in_reply_to_id && !(await Note.fromId(in_reply_to_id))) { if (in_reply_to_id && !(await Note.fromId(in_reply_to_id))) {
return context.json( throw new ApiError(
{ error: "Invalid in_reply_to_id (not found)" },
422, 422,
"Note referenced by in_reply_to_id not found",
); );
} }
if (quote_id && !(await Note.fromId(quote_id))) { if (quote_id && !(await Note.fromId(quote_id))) {
return context.json({ error: "Invalid quote_id (not found)" }, 422); throw new ApiError(422, "Note referenced by quote_id not found");
} }
const newNote = await Note.fromData({ const newNote = await Note.fromData({

View file

@ -4,6 +4,7 @@ import { Note, Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -69,7 +70,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const { objects, link } = await Timeline.getNoteTimeline( const { objects, link } = await Timeline.getNoteTimeline(

View file

@ -4,6 +4,7 @@ import { db } from "@versia/kit/db";
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables"; import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
import { type SQL, and, eq, inArray } from "drizzle-orm"; import { type SQL, and, eq, inArray } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -200,7 +201,7 @@ export default apiRoute((app) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const userFilter = await db.query.Filters.findFirst({ const userFilter = await db.query.Filters.findFirst({
@ -212,7 +213,7 @@ export default apiRoute((app) => {
}); });
if (!userFilter) { if (!userFilter) {
return context.json({ error: "Filter not found" }, 404); throw new ApiError(404, "Filter not found");
} }
return context.json( return context.json(
@ -247,7 +248,7 @@ export default apiRoute((app) => {
} = context.req.valid("json"); } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await db await db
@ -336,7 +337,7 @@ export default apiRoute((app) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
await db await db

View file

@ -4,6 +4,7 @@ import { db } from "@versia/kit/db";
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables"; import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
import type { SQL } from "drizzle-orm"; import type { SQL } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
route: "/api/v2/filters", route: "/api/v2/filters",
@ -136,7 +137,7 @@ export default apiRoute((app) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const userFilters = await db.query.Filters.findMany({ const userFilters = await db.query.Filters.findMany({
@ -178,7 +179,7 @@ export default apiRoute((app) => {
} = context.req.valid("json"); } = context.req.valid("json");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
const newFilter = ( const newFilter = (

View file

@ -4,6 +4,7 @@ import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -82,11 +83,9 @@ export default apiRoute((app) =>
const { file, thumbnail, description } = context.req.valid("form"); const { file, thumbnail, description } = context.req.valid("form");
if (file.size > config.validation.max_media_size) { if (file.size > config.validation.max_media_size) {
return context.json( throw new ApiError(
{
error: `File too large, max size is ${config.validation.max_media_size} bytes`,
},
413, 413,
`File too large, max size is ${config.validation.max_media_size} bytes`,
); );
} }
@ -94,7 +93,11 @@ export default apiRoute((app) =>
config.validation.enforce_mime_types && config.validation.enforce_mime_types &&
!config.validation.allowed_mime_types.includes(file.type) !config.validation.allowed_mime_types.includes(file.type)
) { ) {
return context.json({ error: "Invalid file type" }, 415); throw new ApiError(
415,
`File type ${file.type} is not allowed`,
`Allowed types: ${config.validation.allowed_mime_types.join(", ")}`,
);
} }
const sha256 = new Bun.SHA256(); const sha256 = new Bun.SHA256();

View file

@ -10,6 +10,7 @@ import { Note, User, db } from "@versia/kit/db";
import { Instances, Notes, RolePermissions, Users } from "@versia/kit/tables"; import { Instances, Notes, RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { searchManager } from "~/classes/search/search-manager"; import { searchManager } from "~/classes/search/search-manager";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -95,19 +96,14 @@ export default apiRoute((app) =>
context.req.valid("query"); context.req.valid("query");
if (!self && (resolve || offset)) { if (!self && (resolve || offset)) {
return context.json( throw new ApiError(
{
error: "Cannot use resolve or offset without being authenticated",
},
401, 401,
"Usage of resolve or offset requires authentication",
); );
} }
if (!config.sonic.enabled) { if (!config.sonic.enabled) {
return context.json( throw new ApiError(501, "Search is not enabled on this server");
{ error: "Search is not enabled on this server" },
501,
);
} }
let accountResults: string[] = []; let accountResults: string[] = [];

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -72,7 +73,7 @@ export default apiRoute((app) =>
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
if (!(await file.exists())) { if (!(await file.exists())) {
return context.json({ error: "File not found" }, 404); throw new ApiError(404, "File not found");
} }
// Can't directly copy file into Response because this crashes Bun for now // Can't directly copy file into Response because this crashes Bun for now

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status"; import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -57,9 +58,10 @@ export default apiRoute((app) =>
// Check if URL is valid // Check if URL is valid
if (!URL.canParse(id)) { if (!URL.canParse(id)) {
return context.json( throw new ApiError(
{ error: "Invalid URL (it should be encoded as base64url" },
400, 400,
"Invalid URL",
"Should be encoded as base64url",
); );
} }

View file

@ -8,6 +8,7 @@ import { Like, Note, User } from "@versia/kit/db";
import { Likes, Notes } from "@versia/kit/tables"; import { Likes, Notes } from "@versia/kit/tables";
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema, type KnownEntity } from "~/types/api"; import { ErrorSchema, type KnownEntity } from "~/types/api";
@ -82,7 +83,7 @@ export default apiRoute((app) =>
if (foundObject) { if (foundObject) {
if (!(await foundObject.isViewableByUser(null))) { if (!(await foundObject.isViewableByUser(null))) {
return context.json({ error: "Object not found" }, 404); throw new ApiError(404, "Object not found");
} }
} else { } else {
foundObject = await Like.fromSql( foundObject = await Like.fromSql(
@ -98,18 +99,15 @@ export default apiRoute((app) =>
} }
if (!(foundObject && apiObject)) { if (!(foundObject && apiObject)) {
return context.json({ error: "Object not found" }, 404); throw new ApiError(404, "Object not found");
} }
if (!foundAuthor) { if (!foundAuthor) {
return context.json({ error: "Author not found" }, 404); throw new ApiError(404, "Author not found");
} }
if (foundAuthor?.isRemote()) { if (foundAuthor?.isRemote()) {
return context.json( throw new ApiError(403, "Object is from a remote instance");
{ error: "Cannot view objects from remote instances" },
403,
);
} }
// If base_url uses https and request uses http, rewrite request to use https // If base_url uses https and request uses http, rewrite request to use https
// This fixes reverse proxy errors // This fixes reverse proxy errors

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { User as UserSchema } from "@versia/federation/schemas"; import { User as UserSchema } from "@versia/federation/schemas";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -68,14 +69,11 @@ export default apiRoute((app) =>
const user = await User.fromId(uuid); const user = await User.fromId(uuid);
if (!user) { if (!user) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
if (user.isRemote()) { if (user.isRemote()) {
return context.json( throw new ApiError(403, "User is not on this instance");
{ error: "Cannot view users from remote instances" },
403,
);
} }
// Try to detect a web browser and redirect to the user's profile page // Try to detect a web browser and redirect to the user's profile page

View file

@ -8,6 +8,7 @@ import { Note, User, db } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables"; import { Notes } from "@versia/kit/tables";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -78,14 +79,11 @@ export default apiRoute((app) =>
const author = await User.fromId(uuid); const author = await User.fromId(uuid);
if (!author) { if (!author) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
if (author.isRemote()) { if (author.isRemote()) {
return context.json( throw new ApiError(403, "User is not on this instance");
{ error: "Cannot view users from remote instances" },
403,
);
} }
const pageNumber = Number(context.req.valid("query").page) || 1; const pageNumber = Number(context.req.valid("query").page) || 1;

View file

@ -1,4 +1,10 @@
import { apiRoute, applyConfig, idValidator, webfingerMention } from "@/api"; import {
apiRoute,
applyConfig,
idValidator,
parseUserAddress,
webfingerMention,
} from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import type { ResponseError } from "@versia/federation"; import type { ResponseError } from "@versia/federation";
@ -7,6 +13,7 @@ import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables"; import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -71,25 +78,27 @@ export default apiRoute((app) =>
const host = new URL(config.http.base_url).host; const host = new URL(config.http.base_url).host;
const { username, domain } = parseUserAddress(requestedUser);
// Check if user is a local user // Check if user is a local user
if (requestedUser.split("@")[1] !== host) { if (domain !== host) {
return context.json({ error: "User is a remote user" }, 404); throw new ApiError(
404,
`User domain ${domain} does not match ${host}`,
);
} }
const isUuid = requestedUser.split("@")[0].match(idValidator); const isUuid = username.match(idValidator);
const user = await User.fromSql( const user = await User.fromSql(
and( and(
eq( eq(isUuid ? Users.id : Users.username, username),
isUuid ? Users.id : Users.username,
requestedUser.split("@")[0],
),
isNull(Users.instanceId), isNull(Users.instanceId),
), ),
); );
if (!user) { if (!user) {
return context.json({ error: "User not found" }, 404); throw new ApiError(404, "User not found");
} }
let activityPubUrl = ""; let activityPubUrl = "";

17
app.ts
View file

@ -14,6 +14,7 @@ import { prettyJSON } from "hono/pretty-json";
import { secureHeaders } from "hono/secure-headers"; import { secureHeaders } from "hono/secure-headers";
import pkg from "~/package.json" with { type: "application/json" }; import pkg from "~/package.json" with { type: "application/json" };
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ApiError } from "./classes/errors/api-error.ts";
import { PluginLoader } from "./classes/plugin/loader.ts"; import { PluginLoader } from "./classes/plugin/loader.ts";
import { agentBans } from "./middlewares/agent-bans.ts"; import { agentBans } from "./middlewares/agent-bans.ts";
import { bait } from "./middlewares/bait.ts"; import { bait } from "./middlewares/bait.ts";
@ -192,11 +193,9 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
proxy?.headers.set("Cache-Control", "max-age=31536000"); proxy?.headers.set("Cache-Control", "max-age=31536000");
if (!proxy || proxy.status === 404) { if (!proxy || proxy.status === 404) {
return context.json( throw new ApiError(
{
error: "Route not found on proxy or API route. Are you using the correct HTTP method?",
},
404, 404,
"Route not found on proxy or API route. Are you using the correct HTTP method?",
); );
} }
@ -214,6 +213,16 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
}); });
app.onError((error, c) => { app.onError((error, c) => {
if (error instanceof ApiError) {
return c.json(
{
error: error.message,
details: error.details,
},
error.status,
);
}
serverLogger.error`${error}`; serverLogger.error`${error}`;
sentry?.captureException(error); sentry?.captureException(error);
return c.json( return c.json(

View file

@ -1,9 +1,5 @@
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import { import { EntityValidator, type ResponseError } from "@versia/federation";
EntityValidator,
type ResponseError,
type ValidationError,
} from "@versia/federation";
import type { InstanceMetadata } from "@versia/federation/types"; import type { InstanceMetadata } from "@versia/federation/types";
import { db } from "@versia/kit/db"; import { db } from "@versia/kit/db";
import { Instances } from "@versia/kit/tables"; import { Instances } from "@versia/kit/tables";
@ -17,6 +13,7 @@ import {
inArray, inArray,
} from "drizzle-orm"; } from "drizzle-orm";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ApiError } from "../errors/api-error.ts";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
import { User } from "./user.ts"; import { User } from "./user.ts";
@ -141,14 +138,12 @@ export class Instance extends BaseInterface<typeof Instances> {
public static async fetchMetadata(url: string): Promise<{ public static async fetchMetadata(url: string): Promise<{
metadata: InstanceMetadata; metadata: InstanceMetadata;
protocol: "versia" | "activitypub"; protocol: "versia" | "activitypub";
} | null> { }> {
const origin = new URL(url).origin; const origin = new URL(url).origin;
const wellKnownUrl = new URL("/.well-known/versia", origin); const wellKnownUrl = new URL("/.well-known/versia", origin);
const logger = getLogger(["federation", "resolvers"]);
const requester = await User.getFederationRequester(); const requester = await User.getFederationRequester();
try {
const { ok, raw, data } = await requester const { ok, raw, data } = await requester
.get(wellKnownUrl, { .get(wellKnownUrl, {
// @ts-expect-error Bun extension // @ts-expect-error Bun extension
@ -164,7 +159,10 @@ export class Instance extends BaseInterface<typeof Instances> {
const data = await Instance.fetchActivityPubMetadata(url); const data = await Instance.fetchActivityPubMetadata(url);
if (!data) { if (!data) {
return null; throw new ApiError(
404,
`Instance at ${origin} is not reachable or does not exist`,
);
} }
return { return {
@ -174,22 +172,14 @@ export class Instance extends BaseInterface<typeof Instances> {
} }
try { try {
const metadata = await new EntityValidator().InstanceMetadata( const metadata = await new EntityValidator().InstanceMetadata(data);
data,
);
return { metadata, protocol: "versia" }; return { metadata, protocol: "versia" };
} catch (error) { } catch {
logger.error`Instance ${chalk.bold( throw new ApiError(
origin, 404,
)} has invalid metadata: ${(error as ValidationError).message}`; `Instance at ${origin} has invalid metadata`,
return null; );
}
} catch (error) {
logger.error`Failed to fetch Versia metadata for instance ${chalk.bold(
origin,
)} - Error! ${error}`;
return null;
} }
} }
@ -319,7 +309,7 @@ export class Instance extends BaseInterface<typeof Instances> {
} }
} }
public static resolveFromHost(host: string): Promise<Instance | null> { public static resolveFromHost(host: string): Promise<Instance> {
if (host.startsWith("http")) { if (host.startsWith("http")) {
const url = new URL(host).host; const url = new URL(host).host;
@ -331,8 +321,7 @@ export class Instance extends BaseInterface<typeof Instances> {
return Instance.resolve(url.origin); return Instance.resolve(url.origin);
} }
public static async resolve(url: string): Promise<Instance | null> { public static async resolve(url: string): Promise<Instance> {
const logger = getLogger(["federation", "resolvers"]);
const host = new URL(url).host; const host = new URL(url).host;
const existingInstance = await Instance.fromSql( const existingInstance = await Instance.fromSql(
@ -345,11 +334,6 @@ export class Instance extends BaseInterface<typeof Instances> {
const output = await Instance.fetchMetadata(url); const output = await Instance.fetchMetadata(url);
if (!output) {
logger.error`Failed to resolve instance ${chalk.bold(host)}`;
return null;
}
const { metadata, protocol } = output; const { metadata, protocol } = output;
return Instance.insert({ return Instance.insert({

View file

@ -297,7 +297,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
public async unfollow( public async unfollow(
followee: User, followee: User,
relationship: Relationship, relationship: Relationship,
): Promise<boolean> { ): Promise<void> {
if (followee.isRemote()) { if (followee.isRemote()) {
await deliveryQueue.add(DeliveryJobType.FederateEntity, { await deliveryQueue.add(DeliveryJobType.FederateEntity, {
entity: this.unfollowToVersia(followee), entity: this.unfollowToVersia(followee),
@ -309,8 +309,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await relationship.update({ await relationship.update({
following: false, following: false,
}); });
return true;
} }
private unfollowToVersia(followee: User): Unfollow { private unfollowToVersia(followee: User): Unfollow {

View file

@ -0,0 +1,24 @@
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { JSONObject } from "hono/utils/types";
/**
* API Error
*
* Custom error class used to throw errors in the API. Includes a status code, a message and an optional description.
* @extends Error
*/
export class ApiError extends Error {
/**
* @param {StatusCode} status - The status code of the error
* @param {string} message - The message of the error
* @param {string | JSONObject} [details] - The description of the error
*/
public constructor(
public status: ContentfulStatusCode,
public message: string,
public details?: string | JSONObject,
) {
super(message);
this.name = "ApiError";
}
}

View file

@ -1,4 +1,5 @@
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const agentBans = createMiddleware(async (context, next) => { export const agentBans = createMiddleware(async (context, next) => {
@ -7,7 +8,7 @@ export const agentBans = createMiddleware(async (context, next) => {
for (const agent of config.http.banned_user_agents) { for (const agent of config.http.banned_user_agents) {
if (new RegExp(agent).test(ua)) { if (new RegExp(agent).test(ua)) {
return context.json({ error: "Forbidden" }, 403); throw new ApiError(403, "Forbidden");
} }
} }

View file

@ -1,4 +1,5 @@
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { ApiError } from "~/classes/errors/api-error";
export const boundaryCheck = createMiddleware(async (context, next) => { export const boundaryCheck = createMiddleware(async (context, next) => {
// Checks that FormData boundary is present // Checks that FormData boundary is present
@ -6,11 +7,10 @@ export const boundaryCheck = createMiddleware(async (context, next) => {
if (contentType?.includes("multipart/form-data")) { if (contentType?.includes("multipart/form-data")) {
if (!contentType.includes("boundary")) { if (!contentType.includes("boundary")) {
return context.json( throw new ApiError(
{
error: "You are sending a request with a multipart/form-data content type but without a boundary. Please include a boundary in the Content-Type header. For more information, visit https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data",
},
400, 400,
"Missing FormData boundary",
"You are sending a request with a multipart/form-data content type but without a boundary. Please include a boundary in the Content-Type header. For more information, visit https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data",
); );
} }
} }

View file

@ -3,6 +3,7 @@ import { getLogger } from "@logtape/logtape";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const ipBans = createMiddleware(async (context, next) => { export const ipBans = createMiddleware(async (context, next) => {
@ -18,7 +19,7 @@ export const ipBans = createMiddleware(async (context, next) => {
for (const ip of config.http.banned_ips) { for (const ip of config.http.banned_ips) {
try { try {
if (matches(ip, requestIp?.address)) { if (matches(ip, requestIp?.address)) {
return context.json({ error: "Forbidden" }, 403); throw new ApiError(403, "Forbidden");
} }
} catch (e) { } catch (e) {
const logger = getLogger("server"); const logger = getLogger("server");

View file

@ -5,6 +5,7 @@ import { getCookie } from "hono/cookie";
import { jwtVerify } from "jose"; import { jwtVerify } from "jose";
import { JOSEError, JWTExpired } from "jose/errors"; import { JOSEError, JWTExpired } from "jose/errors";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error.ts";
import { RolePermissions } from "~/drizzle/schema.ts"; import { RolePermissions } from "~/drizzle/schema.ts";
import authorizeRoute from "./routes/authorize.ts"; import authorizeRoute from "./routes/authorize.ts";
import jwksRoute from "./routes/jwks.ts"; import jwksRoute from "./routes/jwks.ts";
@ -108,13 +109,7 @@ plugin.registerRoute("/admin/*", (app) => {
const jwtCookie = getCookie(context, "jwt"); const jwtCookie = getCookie(context, "jwt");
if (!jwtCookie) { if (!jwtCookie) {
return context.json( throw new ApiError(401, "Missing JWT cookie");
{
error: "Unauthorized",
message: "No JWT cookie provided",
},
401,
);
} }
const { keys } = context.get("pluginConfig"); const { keys } = context.get("pluginConfig");
@ -132,22 +127,10 @@ plugin.registerRoute("/admin/*", (app) => {
if (result instanceof JOSEError) { if (result instanceof JOSEError) {
if (result instanceof JWTExpired) { if (result instanceof JWTExpired) {
return context.json( throw new ApiError(401, "JWT has expired");
{
error: "Unauthorized",
message: "JWT has expired. Please log in again.",
},
401,
);
} }
return context.json( throw new ApiError(401, "Invalid JWT");
{
error: "Unauthorized",
message: "Invalid JWT",
},
401,
);
} }
const { const {
@ -155,25 +138,15 @@ plugin.registerRoute("/admin/*", (app) => {
} = result; } = result;
if (!sub) { if (!sub) {
return context.json( throw new ApiError(401, "Invalid JWT (no sub)");
{
error: "Unauthorized",
message: "Invalid JWT (no sub)",
},
401,
);
} }
const user = await User.fromId(sub); const user = await User.fromId(sub);
if (!user?.hasPermission(RolePermissions.ManageInstanceFederation)) { if (!user?.hasPermission(RolePermissions.ManageInstanceFederation)) {
return context.json( throw new ApiError(
{
error: "Unauthorized",
message:
"You do not have permission to access this resource",
},
403, 403,
`Missing '${RolePermissions.ManageInstanceFederation}' permission`,
); );
} }

View file

@ -102,7 +102,7 @@ describe("/oauth/authorize", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid JWT, could not verify", "Invalid JWT: could not verify",
); );
}); });
@ -141,7 +141,7 @@ describe("/oauth/authorize", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid JWT, missing required fields (aud, sub, exp)", "Invalid JWT: missing required fields (aud, sub, exp, iss)",
); );
}); });
@ -183,7 +183,7 @@ describe("/oauth/authorize", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid JWT, sub is not a valid user ID", "Invalid JWT: sub is not a valid user ID",
); );
const jwt2 = await new SignJWT({ const jwt2 = await new SignJWT({
@ -266,9 +266,9 @@ describe("/oauth/authorize", () => {
config.http.base_url, config.http.base_url,
); );
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("unauthorized");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
`User is missing the required permission ${RolePermissions.OAuth}`, `User missing required '${RolePermissions.OAuth}' permission`,
); );
config.permissions.default = oldPermissions; config.permissions.default = oldPermissions;
@ -312,7 +312,7 @@ describe("/oauth/authorize", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid client_id: no associated application found", "Invalid client_id: no associated API application found",
); );
}); });
@ -354,7 +354,7 @@ describe("/oauth/authorize", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_request"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid redirect_uri: does not match application's redirect_uri", "Invalid redirect_uri: does not match API application's redirect_uri",
); );
}); });
@ -394,7 +394,7 @@ describe("/oauth/authorize", () => {
config.http.base_url, config.http.base_url,
); );
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
expect(params.get("error")).toBe("invalid_scope"); expect(params.get("error")).toBe("invalid_request");
expect(params.get("error_description")).toBe( expect(params.get("error_description")).toBe(
"Invalid scope: not a subset of the application's scopes", "Invalid scope: not a subset of the application's scopes",
); );

View file

@ -6,6 +6,7 @@ import { type SQL, and, eq, isNull } from "@versia/kit/drizzle";
import { OpenIdAccounts, RolePermissions, Users } from "@versia/kit/tables"; import { OpenIdAccounts, RolePermissions, Users } from "@versia/kit/tables";
import { setCookie } from "hono/cookie"; import { setCookie } from "hono/cookie";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
import { ApiError } from "~/classes/errors/api-error.ts";
import type { PluginType } from "../../index.ts"; import type { PluginType } from "../../index.ts";
import { automaticOidcFlow } from "../../utils.ts"; import { automaticOidcFlow } from "../../utils.ts";
@ -78,7 +79,7 @@ export default (plugin: PluginType): void => {
.providers.find((provider) => provider.id === issuerParam); .providers.find((provider) => provider.id === issuerParam);
if (!issuer) { if (!issuer) {
return context.json({ error: "Issuer not found" }, 404); throw new ApiError(404, "Issuer not found");
} }
const userInfo = await automaticOidcFlow( const userInfo = await automaticOidcFlow(
@ -303,10 +304,7 @@ export default (plugin: PluginType): void => {
} }
if (!flow.application) { if (!flow.application) {
return context.json( throw new ApiError(500, "Application not found");
{ error: "Application not found" },
500,
);
} }
const code = randomString(32, "hex"); const code = randomString(32, "hex");

View file

@ -4,6 +4,7 @@ import { createRoute, z } from "@hono/zod-openapi";
import { db } from "@versia/kit/db"; import { db } from "@versia/kit/db";
import { type SQL, eq } from "@versia/kit/drizzle"; import { type SQL, eq } from "@versia/kit/drizzle";
import { OpenIdAccounts, RolePermissions } from "@versia/kit/tables"; import { OpenIdAccounts, RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import type { PluginType } from "~/plugins/openid"; import type { PluginType } from "~/plugins/openid";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -66,12 +67,7 @@ export default (plugin: PluginType): void => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json( throw new ApiError(401, "Unauthorized");
{
error: "Unauthorized",
},
401,
);
} }
const issuer = context const issuer = context
@ -96,11 +92,9 @@ export default (plugin: PluginType): void => {
}); });
if (!account) { if (!account) {
return context.json( throw new ApiError(
{
error: "Account not found or is not linked to this issuer",
},
404, 404,
"Account not found or is not linked to this issuer",
); );
} }
@ -163,7 +157,7 @@ export default (plugin: PluginType): void => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); throw new ApiError(401, "Unauthorized");
} }
// Check if issuer exists // Check if issuer exists
@ -189,11 +183,9 @@ export default (plugin: PluginType): void => {
}); });
if (!account) { if (!account) {
return context.json( throw new ApiError(
{
error: "Account not found or is not linked to this issuer",
},
404, 404,
"Account not found or is not linked to this issuer",
); );
} }

View file

@ -6,6 +6,7 @@ import {
generateRandomCodeVerifier, generateRandomCodeVerifier,
} from "oauth4webapi"; } from "oauth4webapi";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
import type { PluginType } from "../../index.ts"; import type { PluginType } from "../../index.ts";
import { oauthDiscoveryRequest, oauthRedirectUri } from "../../utils.ts"; import { oauthDiscoveryRequest, oauthRedirectUri } from "../../utils.ts";
@ -57,12 +58,7 @@ export default (plugin: PluginType): void => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json( throw new ApiError(401, "Unauthorized");
{
error: "Unauthorized",
},
401,
);
} }
const linkedAccounts = await user.getLinkedOidcAccounts( const linkedAccounts = await user.getLinkedOidcAccounts(
@ -133,12 +129,7 @@ export default (plugin: PluginType): void => {
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json( throw new ApiError(401, "Unauthorized");
{
error: "Unauthorized",
},
401,
);
} }
const { issuer: issuerId } = context.req.valid("json"); const { issuer: issuerId } = context.req.valid("json");

View file

@ -25,7 +25,7 @@ describe("API Tests", () => {
const data = await response.json(); const data = await response.json();
expect(data.error).toBeString(); expect(data.error).toBeString();
expect(data.error).toContain("https://stackoverflow.com"); expect(data.details).toContain("https://stackoverflow.com");
}); });
// Now automatically mitigated by the server // Now automatically mitigated by the server

View file

@ -24,6 +24,7 @@ import {
import { type ParsedQs, parse } from "qs"; import { type ParsedQs, parse } from "qs";
import type { z } from "zod"; import type { z } from "zod";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { ApiError } from "~/classes/errors/api-error";
import type { AuthData } from "~/classes/functions/user"; import type { AuthData } from "~/classes/functions/user";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import type { ApiRouteMetadata, HonoEnv, HttpVerb } from "~/types/api"; import type { ApiRouteMetadata, HonoEnv, HttpVerb } from "~/types/api";
@ -162,7 +163,7 @@ const checkPermissions = (
auth: AuthData | null, auth: AuthData | null,
permissionData: ApiRouteMetadata["permissions"], permissionData: ApiRouteMetadata["permissions"],
context: Context, context: Context,
): Response | undefined => { ): void => {
const userPerms = auth?.user const userPerms = auth?.user
? auth.user.getAllPermissions() ? auth.user.getAllPermissions()
: config.permissions.anonymous; : config.permissions.anonymous;
@ -175,11 +176,10 @@ const checkPermissions = (
const missingPerms = requiredPerms.filter( const missingPerms = requiredPerms.filter(
(perm) => !userPerms.includes(perm), (perm) => !userPerms.includes(perm),
); );
return context.json( throw new ApiError(
{
error: `You do not have the required permissions to access this route. Missing: ${missingPerms.join(", ")}`,
},
403, 403,
"Missing permissions",
`Missing: ${missingPerms.join(", ")}`,
); );
} }
}; };
@ -188,7 +188,7 @@ const checkRouteNeedsAuth = (
auth: AuthData | null, auth: AuthData | null,
authData: ApiRouteMetadata["auth"], authData: ApiRouteMetadata["auth"],
context: Context, context: Context,
): Response | AuthData => { ): AuthData => {
if (auth?.user && auth?.token) { if (auth?.user && auth?.token) {
return { return {
user: auth.user, user: auth.user,
@ -200,12 +200,7 @@ const checkRouteNeedsAuth = (
authData.required || authData.required ||
authData.methodOverrides?.[context.req.method as HttpVerb] authData.methodOverrides?.[context.req.method as HttpVerb]
) { ) {
return context.json( throw new ApiError(401, "This route requires authentication");
{
error: "This route requires authentication.",
},
401,
);
} }
return { return {
@ -218,31 +213,25 @@ const checkRouteNeedsAuth = (
export const checkRouteNeedsChallenge = async ( export const checkRouteNeedsChallenge = async (
challengeData: ApiRouteMetadata["challenge"], challengeData: ApiRouteMetadata["challenge"],
context: Context, context: Context,
): Promise<true | Response> => { ): Promise<void> => {
if (!challengeData) { if (!challengeData) {
return true; return;
} }
const challengeSolution = context.req.header("X-Challenge-Solution"); const challengeSolution = context.req.header("X-Challenge-Solution");
if (!challengeSolution) { if (!challengeSolution) {
return context.json( throw new ApiError(
{
error: "This route requires a challenge solution to be sent to it via the X-Challenge-Solution header. Please check the documentation for more information.",
},
401, 401,
"Challenge required",
"This route requires a challenge solution to be sent to it via the X-Challenge-Solution header. Please check the documentation for more information.",
); );
} }
const { challenge_id } = extractParams(challengeSolution); const { challenge_id } = extractParams(challengeSolution);
if (!challenge_id) { if (!challenge_id) {
return context.json( throw new ApiError(401, "The challenge solution provided is invalid.");
{
error: "The challenge solution provided is invalid.",
},
401,
);
} }
const challenge = await db.query.Challenges.findFirst({ const challenge = await db.query.Challenges.findFirst({
@ -250,21 +239,11 @@ export const checkRouteNeedsChallenge = async (
}); });
if (!challenge) { if (!challenge) {
return context.json( throw new ApiError(401, "The challenge solution provided is invalid.");
{
error: "The challenge solution provided is invalid.",
},
401,
);
} }
if (new Date(challenge.expiresAt) < new Date()) { if (new Date(challenge.expiresAt) < new Date()) {
return context.json( throw new ApiError(401, "The challenge provided has expired.");
{
error: "The challenge provided has expired.",
},
401,
);
} }
const isValid = await verifySolution( const isValid = await verifySolution(
@ -273,11 +252,9 @@ export const checkRouteNeedsChallenge = async (
); );
if (!isValid) { if (!isValid) {
return context.json( throw new ApiError(
{
error: "The challenge solution provided is incorrect.",
},
401, 401,
"The challenge solution provided is incorrect.",
); );
} }
@ -286,8 +263,6 @@ export const checkRouteNeedsChallenge = async (
.update(Challenges) .update(Challenges)
.set({ expiresAt: new Date().toISOString() }) .set({ expiresAt: new Date().toISOString() })
.where(eq(Challenges.id, challenge_id)); .where(eq(Challenges.id, challenge_id));
return true;
}; };
export const auth = ( export const auth = (
@ -311,41 +286,19 @@ export const auth = (
user: (await token?.getUser()) ?? null, user: (await token?.getUser()) ?? null,
}; };
// Only exists for type casting, as otherwise weird errors happen with Hono
const fakeResponse = context.json({});
// Authentication check // Authentication check
const authCheck = checkRouteNeedsAuth(auth, authData, context) as const authCheck = checkRouteNeedsAuth(auth, authData, context);
| typeof fakeResponse
| AuthData;
if (authCheck instanceof Response) {
return authCheck;
}
context.set("auth", authCheck); context.set("auth", authCheck);
// Permissions check // Permissions check
if (permissionData) { if (permissionData) {
const permissionCheck = checkPermissions( checkPermissions(auth, permissionData, context);
auth,
permissionData,
context,
);
if (permissionCheck) {
return permissionCheck as typeof fakeResponse;
}
} }
// Challenge check // Challenge check
if (challengeData && config.validation.challenges.enabled) { if (challengeData && config.validation.challenges.enabled) {
const challengeCheck = await checkRouteNeedsChallenge( await checkRouteNeedsChallenge(challengeData, context);
challengeData,
context,
);
if (challengeCheck !== true) {
return challengeCheck as typeof fakeResponse;
}
} }
await next(); await next();