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

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 ISO6391 from "iso-639-1";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -91,13 +92,13 @@ export default apiRoute((app) =>
const { reblogs, notify, languages } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -82,7 +83,7 @@ export default apiRoute((app) =>
// TODO: Add follower/following privacy settings
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -81,7 +82,7 @@ export default apiRoute((app) =>
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
// 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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -63,7 +64,7 @@ export default apiRoute((app) =>
const foundUser = await User.fromId(id);
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);

View file

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

View file

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

View file

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

View file

@ -116,7 +116,9 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
expect(response.status).toBe(403);
const output = await response.json();
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);
const output = await response.json();
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);

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Role, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -114,18 +115,18 @@ export default apiRoute((app) => {
const { id, role_id } = context.req.valid("param");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const targetUser = await User.fromId(id);
const role = await Role.fromId(role_id);
if (!role) {
return context.json({ error: "Role not found" }, 404);
throw new ApiError(404, "Role not found");
}
if (!targetUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
// Priority check
@ -136,11 +137,10 @@ export default apiRoute((app) => {
);
if (role.data.priority > userHighestRole.data.priority) {
return context.json(
{
error: `Cannot assign role '${role.data.name}' with priority ${role.data.priority} to user: your highest role priority is ${userHighestRole.data.priority}`,
},
throw new ApiError(
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");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const targetUser = await User.fromId(id);
const role = await Role.fromId(role_id);
if (!role) {
return context.json({ error: "Role not found" }, 404);
throw new ApiError(404, "Role not found");
}
if (!targetUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
// Priority check
@ -176,11 +176,10 @@ export default apiRoute((app) => {
);
if (role.data.priority > userHighestRole.data.priority) {
return context.json(
{
error: `Cannot remove role '${role.data.name}' with priority ${role.data.priority} from user: your highest role priority is ${userHighestRole.data.priority}`,
},
throw new ApiError(
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 { Role, User } from "@versia/kit/db";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -59,7 +60,7 @@ export default apiRoute((app) => {
const targetUser = await User.fromId(id);
if (!targetUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -96,7 +97,7 @@ export default apiRoute((app) =>
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
const {

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Relationship, User } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -80,13 +81,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
@ -94,9 +95,7 @@ export default apiRoute((app) =>
otherUser,
);
if (!(await self.unfollow(otherUser, foundRelationship))) {
return context.json({ error: "Failed to unfollow user" }, 500);
}
await self.unfollow(otherUser, foundRelationship);
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const user = await User.fromId(id);
if (!user) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -72,13 +73,13 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
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 { type InferSelectModel, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -73,7 +74,7 @@ export default apiRoute((app) =>
const { id: ids } = context.req.valid("query");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
// Find followers of the accounts in "ids", that you also follow
@ -84,7 +85,7 @@ export default apiRoute((app) =>
(
await db.execute(sql<InferSelectModel<typeof Users>>`
SELECT "Users"."id" FROM "Users"
INNER JOIN "Relationships" AS "SelfFollowing"
INNER JOIN "Relationships" AS "SelfFollowing"
ON "SelfFollowing"."subjectId" = "Users"."id"
WHERE "SelfFollowing"."ownerId" = ${self.id}
AND "SelfFollowing"."following" = true

View file

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

View file

@ -4,6 +4,7 @@ import { Instance, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -118,7 +119,7 @@ export default apiRoute((app) =>
const uri = await User.webFinger(manager, username, domain);
if (!uri) {
return context.json({ error: "Account not found" }, 404);
throw new ApiError(404, "Account not found");
}
const foundAccount = await User.resolve(uri);
@ -127,6 +128,6 @@ export default apiRoute((app) =>
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -63,7 +64,7 @@ export default apiRoute((app) =>
const ids = Array.isArray(id) ? id : [id];
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
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 stringComparison from "string-comparison";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -80,7 +81,7 @@ export default apiRoute((app) =>
const { user: self } = context.get("auth");
if (!self && following) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
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 ISO6391 from "iso-639-1";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { contentToHtml } from "~/classes/functions/status";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
@ -213,7 +214,7 @@ export default apiRoute((app) =>
} = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const self = user.data;
@ -257,10 +258,7 @@ export default apiRoute((app) =>
);
if (existingUser) {
return context.json(
{ error: "Username is already taken" },
422,
);
throw new ApiError(422, "Username is already taken");
}
self.username = username;
@ -402,7 +400,7 @@ export default apiRoute((app) =>
const output = await User.fromId(self.id);
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);

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api";
import { generateChallenge } from "@/challenges";
import { createRoute, z } from "@hono/zod-openapi";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -54,10 +55,7 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, async (context) => {
if (!config.validation.challenges.enabled) {
return context.json(
{ error: "Challenges are disabled in config" },
400,
);
throw new ApiError(400, "Challenges are disabled in config");
}
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 { eq } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -212,13 +213,13 @@ export default apiRoute((app) => {
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
throw new ApiError(404, "Emoji not found");
}
// Check if user is admin
@ -226,11 +227,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
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`,
},
throw new ApiError(
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");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
throw new ApiError(404, "Emoji not found");
}
// Check if user is admin
@ -256,11 +256,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
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`,
},
throw new ApiError(
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");
if (!user.hasPermission(RolePermissions.ManageEmojis) && emojiGlobal) {
return context.json(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can make an emoji global or not`,
},
throw new ApiError(
401,
"Missing permissions",
`'${RolePermissions.ManageEmojis}' permission is needed to upload global emojis`,
);
}
@ -293,11 +291,10 @@ export default apiRoute((app) => {
: await mimeLookup(element);
if (!contentType.startsWith("image/")) {
return context.json(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
throw new ApiError(
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");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const emoji = await Emoji.fromId(id);
if (!emoji) {
return context.json({ error: "Emoji not found" }, 404);
throw new ApiError(404, "Emoji not found");
}
// Check if user is admin
@ -348,11 +345,10 @@ export default apiRoute((app) => {
!user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.data.ownerId !== user.data.id
) {
return context.json(
{
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`,
},
throw new ApiError(
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 { and, eq, isNull, or } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -117,15 +118,14 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
if (!user.hasPermission(RolePermissions.ManageEmojis) && global) {
return context.json(
{
error: `Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
},
throw new ApiError(
401,
"Missing permissions",
`Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
);
}
@ -139,11 +139,10 @@ export default apiRoute((app) =>
);
if (existing) {
return context.json(
{
error: `An emoji with the shortcode ${shortcode} already exists, either owned by you or global.`,
},
throw new ApiError(
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);
if (!contentType.startsWith("image/")) {
return context.json(
{
error: `Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
},
throw new ApiError(
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 { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -66,7 +67,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const { account_id } = context.req.valid("param");
@ -74,7 +75,7 @@ export default apiRoute((app) =>
const account = await User.fromId(account_id);
if (!account) {
return context.json({ error: "Account not found" }, 404);
throw new ApiError(404, "Account not found");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -66,7 +67,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const { account_id } = context.req.valid("param");
@ -74,7 +75,7 @@ export default apiRoute((app) =>
const account = await User.fromId(account_id);
if (!account) {
return context.json({ error: "Account not found" }, 404);
throw new ApiError(404, "Account not found");
}
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 { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const { objects: followRequests, link } =

View file

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

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -122,7 +123,7 @@ export default apiRoute((app) => {
const attachment = await Attachment.fromId(id);
if (!attachment) {
return context.json({ error: "Media not found" }, 404);
throw new ApiError(404, "Media not found");
}
const { description, thumbnail } = context.req.valid("form");
@ -159,7 +160,7 @@ export default apiRoute((app) => {
const attachment = await Attachment.fromId(id);
if (!attachment) {
return context.json({ error: "Media not found" }, 404);
throw new ApiError(404, "Media not found");
}
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 sharp from "sharp";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -90,11 +91,9 @@ export default apiRoute((app) =>
const { file, thumbnail, description } = context.req.valid("form");
if (file.size > config.validation.max_media_size) {
return context.json(
{
error: `File too large, max size is ${config.validation.max_media_size} bytes`,
},
throw new ApiError(
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.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();

View file

@ -4,6 +4,7 @@ import { Timeline, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -64,7 +65,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -55,13 +56,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const notification = await Notification.fromId(id);
if (!notification) {
return context.json({ error: "Notification not found" }, 404);
throw new ApiError(404, "Notification not found");
}
await notification.update({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -144,7 +144,9 @@ describe("/api/v1/roles/:id", () => {
expect(response.status).toBe(403);
const output = await response.json();
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);
const output = await response.json();
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);
const output = await response.json();
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -156,13 +157,13 @@ export default apiRoute((app) => {
const { id } = context.req.valid("param");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const role = await Role.fromId(id);
if (!role) {
return context.json({ error: "Role not found" }, 404);
throw new ApiError(404, "Role not found");
}
return context.json(role.toApi(), 200);
@ -175,13 +176,13 @@ export default apiRoute((app) => {
context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const role = await Role.fromId(id);
if (!role) {
return context.json({ error: "Role not found" }, 404);
throw new ApiError(404, "Role not found");
}
// Priority check
@ -192,11 +193,10 @@ export default apiRoute((app) => {
);
if (role.data.priority > userHighestRole.data.priority) {
return context.json(
{
error: `Cannot edit role '${role.data.name}' with priority ${role.data.priority}: your highest role priority is ${userHighestRole.data.priority}`,
},
throw new ApiError(
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));
if (!hasPermissions) {
return context.json(
{
error: `You cannot add or remove the following permissions you do not yourself have: ${permissions.join(", ")}`,
},
throw new ApiError(
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");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const role = await Role.fromId(id);
if (!role) {
return context.json({ error: "Role not found" }, 404);
throw new ApiError(404, "Role not found");
}
// Priority check
@ -251,11 +250,10 @@ export default apiRoute((app) => {
);
if (role.data.priority > userHighestRole.data.priority) {
return context.json(
{
error: `Cannot delete role '${role.data.name}' with priority ${role.data.priority}: your highest role priority is ${userHighestRole.data.priority}`,
},
throw new ApiError(
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);
const output = await response.json();
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);
const output = await response.json();
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 { createRoute, z } from "@hono/zod-openapi";
import { Role } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { RolePermissions } from "~/drizzle/schema";
import { ErrorSchema } from "~/types/api";
@ -96,7 +97,7 @@ export default apiRoute((app) => {
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const roles = await Role.getAll();
@ -113,7 +114,7 @@ export default apiRoute((app) => {
context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
// Priority check
@ -124,11 +125,9 @@ export default apiRoute((app) => {
);
if (priority > userHighestRole.data.priority) {
return context.json(
{
error: "You cannot create a role with higher priority than your own",
},
throw new ApiError(
403,
"Cannot create role with higher priority than your own",
);
}
@ -140,11 +139,10 @@ export default apiRoute((app) => {
).every((p) => userPermissions.includes(p));
if (!hasPermissions) {
return context.json(
{
error: `You cannot create a role with the following permissions you do not yourself have: ${permissions.join(", ")}`,
},
throw new ApiError(
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -65,7 +66,7 @@ export default apiRoute((app) =>
const foundStatus = await Note.fromId(id, user?.id);
if (!foundStatus) {
return context.json({ error: "Record not found" }, 404);
throw new ApiError(404, "Note not found");
}
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 { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -68,13 +69,13 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const note = await Note.fromId(id, user?.id);
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);

View file

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

View file

@ -4,6 +4,7 @@ import { Attachment, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -216,7 +217,7 @@ export default apiRoute((app) => {
const note = await Note.fromId(id, user?.id);
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);
@ -229,11 +230,11 @@ export default apiRoute((app) => {
const note = await Note.fromId(id, user?.id);
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) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
// TODO: Delete and redraft
@ -249,17 +250,17 @@ export default apiRoute((app) => {
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
const note = await Note.fromId(id, user?.id);
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) {
return context.json({ error: "Unauthorized" }, 401);
throw new ApiError(401, "Unauthorized");
}
// TODO: Polls
@ -275,7 +276,10 @@ export default apiRoute((app) => {
media_ids.length > 0 ? await Attachment.fromIds(media_ids) : [];
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({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { Attachment } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import sharp from "sharp";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -82,11 +83,9 @@ export default apiRoute((app) =>
const { file, thumbnail, description } = context.req.valid("form");
if (file.size > config.validation.max_media_size) {
return context.json(
{
error: `File too large, max size is ${config.validation.max_media_size} bytes`,
},
throw new ApiError(
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.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();

View file

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

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -72,7 +73,7 @@ export default apiRoute((app) =>
const buffer = await file.arrayBuffer();
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

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -57,9 +58,10 @@ export default apiRoute((app) =>
// Check if URL is valid
if (!URL.canParse(id)) {
return context.json(
{ error: "Invalid URL (it should be encoded as base64url" },
throw new ApiError(
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 { and, eq, inArray, sql } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema, type KnownEntity } from "~/types/api";
@ -82,7 +83,7 @@ export default apiRoute((app) =>
if (foundObject) {
if (!(await foundObject.isViewableByUser(null))) {
return context.json({ error: "Object not found" }, 404);
throw new ApiError(404, "Object not found");
}
} else {
foundObject = await Like.fromSql(
@ -98,18 +99,15 @@ export default apiRoute((app) =>
}
if (!(foundObject && apiObject)) {
return context.json({ error: "Object not found" }, 404);
throw new ApiError(404, "Object not found");
}
if (!foundAuthor) {
return context.json({ error: "Author not found" }, 404);
throw new ApiError(404, "Author not found");
}
if (foundAuthor?.isRemote()) {
return context.json(
{ error: "Cannot view objects from remote instances" },
403,
);
throw new ApiError(403, "Object is from a remote instance");
}
// If base_url uses https and request uses http, rewrite request to use https
// 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 } from "@versia/kit/db";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
@ -68,14 +69,11 @@ export default apiRoute((app) =>
const user = await User.fromId(uuid);
if (!user) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
if (user.isRemote()) {
return context.json(
{ error: "Cannot view users from remote instances" },
403,
);
throw new ApiError(403, "User is not on this instance");
}
// 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 { and, eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -78,14 +79,11 @@ export default apiRoute((app) =>
const author = await User.fromId(uuid);
if (!author) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
if (author.isRemote()) {
return context.json(
{ error: "Cannot view users from remote instances" },
403,
);
throw new ApiError(403, "User is not on this instance");
}
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 { getLogger } from "@logtape/logtape";
import type { ResponseError } from "@versia/federation";
@ -7,6 +13,7 @@ import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -71,25 +78,27 @@ export default apiRoute((app) =>
const host = new URL(config.http.base_url).host;
const { username, domain } = parseUserAddress(requestedUser);
// Check if user is a local user
if (requestedUser.split("@")[1] !== host) {
return context.json({ error: "User is a remote user" }, 404);
if (domain !== host) {
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(
and(
eq(
isUuid ? Users.id : Users.username,
requestedUser.split("@")[0],
),
eq(isUuid ? Users.id : Users.username, username),
isNull(Users.instanceId),
),
);
if (!user) {
return context.json({ error: "User not found" }, 404);
throw new ApiError(404, "User not found");
}
let activityPubUrl = "";