refactor(api): ♻️ Refactor more routes to use OpenAPI

This commit is contained in:
Jesse Wierzbinski 2024-08-27 20:14:10 +02:00
parent 5554038f44
commit 6ed1bd747f
No known key found for this signature in database
14 changed files with 1317 additions and 884 deletions

View file

@ -1,10 +1,11 @@
import { apiRoute, applyConfig, auth, handleZodError, qsQuery } from "@/api"; import { apiRoute, applyConfig, auth, qsQuery } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { inArray } from "drizzle-orm"; import { inArray } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -28,14 +29,38 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/v1/accounts/familiar_followers",
summary: "Get familiar followers",
description:
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
middleware: [auth(meta.auth, meta.permissions), qsQuery()],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Familiar followers",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
qsQuery(),
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
const { id: ids } = context.req.valid("query"); const { id: ids } = context.req.valid("query");
@ -43,8 +68,7 @@ export default apiRoute((app) =>
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }
const idFollowerRelationships = const idFollowerRelationships = await db.query.Relationships.findMany({
await db.query.Relationships.findMany({
columns: { columns: {
ownerId: true, ownerId: true,
}, },
@ -59,12 +83,11 @@ export default apiRoute((app) =>
}); });
if (idFollowerRelationships.length === 0) { if (idFollowerRelationships.length === 0) {
return context.json([]); return context.json([], 200);
} }
// Find users that you follow in idFollowerRelationships // Find users that you follow in idFollowerRelationships
const relevantRelationships = await db.query.Relationships.findMany( const relevantRelationships = await db.query.Relationships.findMany({
{
columns: { columns: {
subjectId: true, subjectId: true,
}, },
@ -77,11 +100,10 @@ export default apiRoute((app) =>
), ),
eq(relationship.following, true), eq(relationship.following, true),
), ),
}, });
);
if (relevantRelationships.length === 0) { if (relevantRelationships.length === 0) {
return context.json([]); return context.json([], 200);
} }
const finalUsers = await User.manyFromSql( const finalUsers = await User.manyFromSql(
@ -91,7 +113,9 @@ export default apiRoute((app) =>
), ),
); );
return context.json(finalUsers.map((o) => o.toApi())); return context.json(
}, finalUsers.map((o) => o.toApi()),
), 200,
);
}),
); );

View file

@ -1,9 +1,10 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -27,13 +28,37 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/v1/accounts/id",
summary: "Get account by username",
description: "Get an account by username",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
404: {
description: "Not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { username } = context.req.valid("query"); const { username } = context.req.valid("query");
const user = await User.fromSql( const user = await User.fromSql(
@ -44,7 +69,6 @@ export default apiRoute((app) =>
return context.json({ error: "User not found" }, 404); return context.json({ error: "User not found" }, 404);
} }
return context.json(user.toApi()); return context.json(user.toApi(), 200);
}, }),
),
); );

View file

@ -1,7 +1,6 @@
import { apiRoute, applyConfig, auth, handleZodError, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { response } from "@/response";
import { tempmailDomains } from "@/tempmail"; import { tempmailDomains } from "@/tempmail";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
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";
@ -39,14 +38,104 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/v1/accounts",
summary: "Create account",
description: "Register a new account",
middleware: [auth(meta.auth, meta.permissions, meta.challenge)],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Account created",
},
422: {
description: "Validation failed",
content: {
"application/json": {
schema: z.object({
error: z.string(),
details: z.object({
username: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_TOO_LONG",
"ERR_TOO_SHORT",
"ERR_BLOCKED",
"ERR_TAKEN",
"ERR_RESERVED",
"ERR_ACCEPTED",
"ERR_INCLUSION",
]),
description: z.string(),
}),
),
email: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_BLOCKED",
"ERR_TAKEN",
]),
description: z.string(),
}),
),
password: z.array(
z.object({
error: z.enum([
"ERR_BLANK",
"ERR_INVALID",
"ERR_TOO_LONG",
"ERR_TOO_SHORT",
]),
description: z.string(),
}),
),
agreement: z.array(
z.object({
error: z.enum(["ERR_ACCEPTED"]),
description: z.string(),
}),
),
locale: z.array(
z.object({
error: z.enum(["ERR_BLANK", "ERR_INVALID"]),
description: z.string(),
}),
),
reason: z.array(
z.object({
error: z.enum(["ERR_BLANK"]),
description: z.string(),
}),
),
}),
}),
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
auth(meta.auth, meta.permissions, meta.challenge),
jsonOrForm(),
zValidator("json", schemas.json, handleZodError),
async (context) => {
const form = context.req.valid("json"); const form = context.req.valid("json");
const { username, email, password, agreement, locale } = const { username, email, password, agreement, locale } =
context.req.valid("json"); context.req.valid("json");
@ -116,11 +205,7 @@ export default apiRoute((app) =>
} }
// Check if username doesnt match filters // Check if username doesnt match filters
if ( if (config.filters.username.some((filter) => username?.match(filter))) {
config.filters.username.some((filter) =>
username?.match(filter),
)
) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: "contains blocked words", description: "contains blocked words",
@ -180,9 +265,7 @@ export default apiRoute((app) =>
if ( if (
config.validation.email_blacklist.includes(email) || config.validation.email_blacklist.includes(email) ||
(config.validation.blacklist_tempmail && (config.validation.blacklist_tempmail &&
tempmailDomains.domains.includes( tempmailDomains.domains.includes((email ?? "").split("@")[1]))
(email ?? "").split("@")[1],
))
) { ) {
errors.details.email.push({ errors.details.email.push({
error: "ERR_BLOCKED", error: "ERR_BLOCKED",
@ -221,9 +304,7 @@ export default apiRoute((app) =>
} }
// If any errors are present, return them // If any errors are present, return them
if ( if (Object.values(errors.details).some((value) => value.length > 0)) {
Object.values(errors.details).some((value) => value.length > 0)
) {
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted" // Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
const errorsText = Object.entries(errors.details) const errorsText = Object.entries(errors.details)
@ -254,7 +335,6 @@ export default apiRoute((app) =>
email: email ?? "", email: email ?? "",
}); });
return response(null, 200); return context.newResponse(null, 200);
}, }),
),
); );

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
anyOf, anyOf,
@ -15,6 +15,7 @@ import {
import { z } from "zod"; import { z } from "zod";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -38,20 +39,40 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/v1/accounts/lookup",
summary: "Lookup account",
description: "Lookup an account by acct",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
404: {
description: "Not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { acct } = context.req.valid("query"); const { acct } = context.req.valid("query");
const { user } = context.get("auth"); const { user } = context.get("auth");
if (!acct) {
return context.json({ error: "Invalid acct parameter" }, 400);
}
// Check if acct is matching format username@domain.com or @username@domain.com // Check if acct is matching format username@domain.com or @username@domain.com
const accountMatches = acct?.trim().match( const accountMatches = acct?.trim().match(
createRegExp( createRegExp(
@ -83,7 +104,7 @@ export default apiRoute((app) =>
const foundAccount = await User.resolve(uri); const foundAccount = await User.resolve(uri);
if (foundAccount) { if (foundAccount) {
return context.json(foundAccount.toApi()); return context.json(foundAccount.toApi(), 200);
} }
return context.json({ error: "Account not found" }, 404); return context.json({ error: "Account not found" }, 404);
@ -97,13 +118,12 @@ export default apiRoute((app) =>
const account = await User.fromSql(eq(Users.username, username)); const account = await User.fromSql(eq(Users.username, username));
if (account) { if (account) {
return context.json(account.toApi()); return context.json(account.toApi(), 200);
} }
return context.json( return context.json(
{ error: `Account with username ${username} not found` }, { error: `Account with username ${username} not found` },
404, 404,
); );
}, }),
),
); );

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig, auth, handleZodError, qsQuery } from "@/api"; import { apiRoute, applyConfig, auth, qsQuery } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { z } from "zod"; import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship"; import { Relationship } from "~/packages/database-interface/relationship";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -26,14 +27,37 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/v1/accounts/relationships",
summary: "Get relationships",
description: "Get relationships by account ID",
middleware: [auth(meta.auth, meta.permissions), qsQuery()],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Relationships",
content: {
"application/json": {
schema: z.array(Relationship.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
qsQuery(),
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
const { id } = context.req.valid("query"); const { id } = context.req.valid("query");
@ -50,11 +74,12 @@ export default apiRoute((app) =>
relationships.sort( relationships.sort(
(a, b) => (a, b) =>
ids.indexOf(a.data.subjectId) - ids.indexOf(a.data.subjectId) - ids.indexOf(b.data.subjectId),
ids.indexOf(b.data.subjectId),
); );
return context.json(relationships.map((r) => r.toApi())); return context.json(
}, relationships.map((r) => r.toApi()),
), 200,
);
}),
); );

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { eq, ilike, not, or, sql } from "drizzle-orm"; import { eq, ilike, not, or, sql } from "drizzle-orm";
import { import {
anyOf, anyOf,
@ -16,6 +16,7 @@ import stringComparison from "string-comparison";
import { z } from "zod"; import { z } from "zod";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -67,13 +68,37 @@ export const schemas = {
}), }),
}; };
export const route = createRoute({
method: "get",
path: "/api/v1/accounts/search",
summary: "Search accounts",
description: "Search for accounts",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Accounts",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { q, limit, offset, resolve, following } = const { q, limit, offset, resolve, following } =
context.req.valid("query"); context.req.valid("query");
const { user: self } = context.get("auth"); const { user: self } = context.get("auth");
@ -123,7 +148,9 @@ export default apiRoute((app) =>
const result = indexOfCorrectSort.map((index) => accounts[index]); const result = indexOfCorrectSort.map((index) => accounts[index]);
return context.json(result.map((acct) => acct.toApi())); return context.json(
}, result.map((acct) => acct.toApi()),
), 200,
);
}),
); );

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, auth, handleZodError, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
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";
@ -12,6 +12,7 @@ import { config } from "~/packages/config-manager/index";
import { Attachment } from "~/packages/database-interface/attachment"; import { Attachment } from "~/packages/database-interface/attachment";
import { Emoji } from "~/packages/database-interface/emoji"; import { Emoji } from "~/packages/database-interface/emoji";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["PATCH"], allowedMethods: ["PATCH"],
@ -125,14 +126,59 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "patch",
path: "/api/v1/accounts/update_credentials",
summary: "Update credentials",
description: "Update user credentials",
middleware: [auth(meta.auth, meta.permissions), jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Updated user",
content: {
"application/json": {
schema: User.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
422: {
description: "Validation error",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
500: {
description: "Couldn't edit user",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
jsonOrForm(),
zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
const { const {
display_name, display_name,
@ -194,7 +240,7 @@ export default apiRoute((app) =>
if (existingUser) { if (existingUser) {
return context.json( return context.json(
{ error: "Username is already taken" }, { error: "Username is already taken" },
400, 422,
); );
} }
@ -288,11 +334,9 @@ export default apiRoute((app) =>
await Emoji.parseFromText(sanitizedDisplayName); await Emoji.parseFromText(sanitizedDisplayName);
const noteEmojis = await Emoji.parseFromText(self.note); const noteEmojis = await Emoji.parseFromText(self.note);
self.emojis = [ self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis].map(
...displaynameEmojis, (e) => e.data,
...noteEmojis, );
...fieldEmojis,
].map((e) => e.data);
// Deduplicate emojis // Deduplicate emojis
self.emojis = self.emojis.filter( self.emojis = self.emojis.filter(
@ -340,7 +384,6 @@ export default apiRoute((app) =>
return context.json({ error: "Couldn't edit user" }, 500); return context.json({ error: "Couldn't edit user" }, 500);
} }
return context.json(output.toApi()); return context.json(output.toApi(), 200);
}, }),
),
); );

View file

@ -1,4 +1,7 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -13,12 +16,34 @@ export const meta = applyConfig({
}, },
}); });
const route = createRoute({
method: "get",
path: "/api/v1/accounts/verify_credentials",
summary: "Verify credentials",
description: "Get your own account information",
middleware: [auth(meta.auth)],
responses: {
200: {
description: "Account",
content: {
"application/json": {
schema: User.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, (context) => {
meta.allowedMethods,
meta.route,
auth(meta.auth, meta.permissions),
(context) => {
// TODO: Add checks for disabled/unverified accounts // TODO: Add checks for disabled/unverified accounts
const { user } = context.get("auth"); const { user } = context.get("auth");
@ -26,7 +51,6 @@ export default apiRoute((app) =>
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }
return context.json(user.toApi(true)); return context.json(user.toApi(true), 200);
}, }),
),
); );

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, handleZodError, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, jsonOrForm } from "@/api";
import { randomString } from "@/math"; import { randomString } from "@/math";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { z } from "zod"; import { z } from "zod";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Applications, RolePermissions } from "~/drizzle/schema"; import { Applications, RolePermissions } from "~/drizzle/schema";
@ -42,13 +42,43 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/v1/apps",
summary: "Create app",
description: "Create an OAuth2 app",
middleware: [jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "App",
content: {
"application/json": {
schema: z.object({
id: z.string().uuid(),
name: z.string(),
website: z.string().nullable(),
client_id: z.string(),
client_secret: z.string(),
redirect_uri: z.string(),
vapid_link: z.string().nullable(),
}),
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
jsonOrForm(),
zValidator("json", schemas.json, handleZodError),
async (context) => {
const { client_name, redirect_uris, scopes, website } = const { client_name, redirect_uris, scopes, website } =
context.req.valid("json"); context.req.valid("json");
@ -66,7 +96,8 @@ export default apiRoute((app) =>
.returning() .returning()
)[0]; )[0];
return context.json({ return context.json(
{
id: app.id, id: app.id,
name: app.name, name: app.name,
website: app.website, website: app.website,
@ -74,7 +105,8 @@ export default apiRoute((app) =>
client_secret: app.secret, client_secret: app.secret,
redirect_uri: app.redirectUri, redirect_uri: app.redirectUri,
vapid_link: app.vapidKey, vapid_link: app.vapidKey,
});
}, },
), 200,
);
}),
); );

View file

@ -1,6 +1,8 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { getFromToken } from "~/classes/functions/application"; import { getFromToken } from "~/classes/functions/application";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -17,12 +19,40 @@ export const meta = applyConfig({
}, },
}); });
const route = createRoute({
method: "get",
path: "/api/v1/apps/verify_credentials",
summary: "Verify credentials",
description: "Get your own application information",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Application",
content: {
"application/json": {
schema: z.object({
name: z.string(),
website: z.string().nullable(),
vapid_key: z.string().nullable(),
redirect_uris: z.string(),
scopes: z.string(),
}),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
auth(meta.auth, meta.permissions),
async (context) => {
const { user, token } = context.get("auth"); const { user, token } = context.get("auth");
if (!token) { if (!token) {
@ -38,13 +68,15 @@ export default apiRoute((app) =>
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }
return context.json({ return context.json(
{
name: application.name, name: application.name,
website: application.website, website: application.website,
vapid_key: application.vapidKey, vapid_key: application.vapidKey,
redirect_uris: application.redirectUri, redirect_uris: application.redirectUri,
scopes: application.scopes, scopes: application.scopes,
});
}, },
), 200,
);
}),
); );

View file

@ -1,15 +1,11 @@
import { import { apiRoute, applyConfig, auth, idValidator } from "@/api";
apiRoute, import { createRoute } from "@hono/zod-openapi";
applyConfig,
auth,
handleZodError,
idValidator,
} from "@/api";
import { zValidator } from "@hono/zod-validator";
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 { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { Timeline } from "~/packages/database-interface/timeline"; import { Timeline } from "~/packages/database-interface/timeline";
import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -36,15 +32,38 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/v1/blocks",
summary: "Get blocks",
description: "Get users you have blocked",
middleware: [auth(meta.auth, meta.permissions)],
request: {
query: schemas.query,
},
responses: {
200: {
description: "Blocks",
content: {
"application/json": {
schema: z.array(User.schema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods, const { max_id, since_id, min_id, limit } = context.req.valid("query");
meta.route,
zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { max_id, since_id, min_id, limit } =
context.req.valid("query");
const { user } = context.get("auth"); const { user } = context.get("auth");
@ -70,6 +89,5 @@ export default apiRoute((app) =>
Link: link, Link: link,
}, },
); );
}, }),
),
); );

View file

@ -1,6 +1,8 @@
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 { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -17,12 +19,41 @@ export const meta = applyConfig({
}, },
}); });
const route = createRoute({
method: "post",
path: "/api/v1/challenges",
summary: "Generate a challenge",
description: "Generate a challenge to solve",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Challenge",
content: {
"application/json": {
schema: z.object({
id: z.string(),
algorithm: z.enum(["SHA-1", "SHA-256", "SHA-512"]),
challenge: z.string(),
maxnumber: z.number().optional(),
salt: z.string(),
signature: z.string(),
}),
},
},
},
400: {
description: "Challenges are disabled",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
auth(meta.auth, meta.permissions),
async (context) => {
if (!config.validation.challenges.enabled) { if (!config.validation.challenges.enabled) {
return context.json( return context.json(
{ error: "Challenges are disabled in config" }, { error: "Challenges are disabled in config" },
@ -32,10 +63,12 @@ export default apiRoute((app) =>
const result = await generateChallenge(); const result = await generateChallenge();
return context.json({ return context.json(
{
id: result.id, id: result.id,
...result.challenge, ...result.challenge,
});
}, },
), 200,
);
}),
); );

View file

@ -1,4 +1,5 @@
import { apiRoute, applyConfig, auth } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { and, eq, isNull, or } from "drizzle-orm"; import { and, eq, isNull, or } from "drizzle-orm";
import { Emojis, RolePermissions } from "~/drizzle/schema"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { Emoji } from "~/packages/database-interface/emoji"; import { Emoji } from "~/packages/database-interface/emoji";
@ -18,12 +19,26 @@ export const meta = applyConfig({
}, },
}); });
const route = createRoute({
method: "get",
path: "/api/v1/custom_emojis",
summary: "Get custom emojis",
description: "Get custom emojis",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "Emojis",
content: {
"application/json": {
schema: z.array(Emoji.schema),
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
auth(meta.auth, meta.permissions),
async (context) => {
const { user } = context.get("auth"); const { user } = context.get("auth");
const emojis = await Emoji.manyFromSql( const emojis = await Emoji.manyFromSql(
@ -36,7 +51,9 @@ export default apiRoute((app) =>
), ),
); );
return context.json(emojis.map((emoji) => emoji.toApi())); return context.json(
}, emojis.map((emoji) => emoji.toApi()),
), 200,
);
}),
); );

View file

@ -1,13 +1,6 @@
import { import { apiRoute, applyConfig, auth, emojiValidator, jsonOrForm } from "@/api";
apiRoute,
applyConfig,
auth,
emojiValidator,
handleZodError,
jsonOrForm,
} from "@/api";
import { mimeLookup } from "@/content_types"; import { mimeLookup } from "@/content_types";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
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 { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
@ -15,6 +8,7 @@ import { Emojis, RolePermissions } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Attachment } from "~/packages/database-interface/attachment"; import { Attachment } from "~/packages/database-interface/attachment";
import { Emoji } from "~/packages/database-interface/emoji"; import { Emoji } from "~/packages/database-interface/emoji";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -59,14 +53,57 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/v1/emojis",
summary: "Upload emoji",
description: "Upload an emoji",
middleware: [auth(meta.auth, meta.permissions), jsonOrForm()],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "uploaded emoji",
content: {
"application/json": {
schema: Emoji.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
422: {
description: "Invalid data",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
jsonOrForm(),
zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { shortcode, element, alt, global, category } = const { shortcode, element, alt, global, category } =
context.req.valid("json"); context.req.valid("json");
const { user } = context.get("auth"); const { user } = context.get("auth");
@ -106,9 +143,7 @@ export default apiRoute((app) =>
// Check of emoji is an image // Check of emoji is an image
let contentType = let contentType =
element instanceof File element instanceof File ? element.type : await mimeLookup(element);
? element.type
: await mimeLookup(element);
if (!contentType.startsWith("image/")) { if (!contentType.startsWith("image/")) {
return context.json( return context.json(
@ -140,7 +175,6 @@ export default apiRoute((app) =>
alt, alt,
}); });
return context.json(emoji.toApi()); return context.json(emoji.toApi(), 200);
}, }),
),
); );