From be881f18cd6536296d2c56e1c063246c0692ab53 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 17 Jul 2024 14:02:29 +0200 Subject: [PATCH] feat(api): :sparkles: Add new endpoint to get a user by its username --- CHANGELOG.md | 1 + bun.lockb | Bin 258436 -> 258436 bytes docs/api/frontend.md | 18 +++++++ docs/api/mastodon.md | 9 +++- package.json | 2 +- packages/database-interface/user.ts | 1 + server/api/api/v1/accounts/:id/index.test.ts | 1 + server/api/api/v1/accounts/id/index.test.ts | 44 ++++++++++++++++ server/api/api/v1/accounts/id/index.ts | 51 +++++++++++++++++++ 9 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 server/api/api/v1/accounts/id/index.test.ts create mode 100644 server/api/api/v1/accounts/id/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e9a692..a63a9724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Lysand Server `0.7.0` is backwards compatible with `0.6.0`. However, some new fe - Added [**TOS and Privacy Policy**](docs/api/mastodon.md) endpoints. - Added [**Challenge API**](docs/api/challenges.md). (basically CAPTCHAS). This can be enabled/disabled by administrators. No `lysand-fe` support yet. - Added ability to change the `username` of a user. ([Mastodon API extension](docs/api/mastodon.md)). +- Added an endpoint to get a user by its username. - Add OpenID Connect registration support. Admins can now disable username/password registration entirely and still allow users to sign up via OpenID Connect. - Add option to never convert vector images to a raster format. - Refactor logging system to be more robust and easier to use. Logfiles are now automatically rotated. diff --git a/bun.lockb b/bun.lockb index 6f47e0eba497cc78650c4f6a0fa56ad470e602e5..48afa614c1c0f5ec0e7f10ed76284cee032a5bc5 100755 GIT binary patch delta 154 zcmV;L0A>G#;}3-650EY(W)gr@=_xZ>7dG|^O#=m_TXSji?Sm@{|4V#_Dd4b&u}&&< zlXy5Vv#4~1;y_S$L@B`P?EPS2tM0C?^5aEid0=FgrUI~5x3Y(AR{^(eR|1Me0W`Pxn*x921vD;nXL^^gfCCw~ Im4E~9a1KjJi~s-t delta 154 zcmV;L0A>G#;}3-650EY(XbwE_4!9+QY)+}C(#r-PC<)A}ZA>}+%_92DRH15au}&&< zlQ=6dv#4~1;y^I!^1$61?SHF { username: user.username, display_name: user.displayName, note: user.note, + uri: this.getUri(), url: user.uri || new URL(`/@${user.username}`, config.http.base_url).toString(), diff --git a/server/api/api/v1/accounts/:id/index.test.ts b/server/api/api/v1/accounts/:id/index.test.ts index 75466da1..e43a578e 100644 --- a/server/api/api/v1/accounts/:id/index.test.ts +++ b/server/api/api/v1/accounts/:id/index.test.ts @@ -72,6 +72,7 @@ describe(meta.route, () => { statuses_count: 40, note: users[0].data.note, acct: users[0].data.username, + uri: expect.any(String), url: expect.any(String), avatar_static: expect.any(String), header_static: expect.any(String), diff --git a/server/api/api/v1/accounts/id/index.test.ts b/server/api/api/v1/accounts/id/index.test.ts new file mode 100644 index 00000000..4ce0dcd5 --- /dev/null +++ b/server/api/api/v1/accounts/id/index.test.ts @@ -0,0 +1,44 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import type { Account as ApiAccount } from "@lysand-org/client/types"; +import { config } from "config-manager"; +import { getTestUsers, sendTestRequest } from "~/tests/utils"; +import { meta } from "./index"; + +const { users, deleteUsers } = await getTestUsers(5); + +afterAll(async () => { + await deleteUsers(); +}); + +// /api/v1/accounts/id +describe(meta.route, () => { + test("should correctly get user from username", async () => { + const response = await sendTestRequest( + new Request( + new URL( + `${meta.route}?username=${users[0].data.username}`, + config.http.base_url, + ), + ), + ); + + expect(response.status).toBe(200); + + const data = (await response.json()) as ApiAccount; + + expect(data.id).toBe(users[0].id); + }); + + test("should return 404 for non-existent user", async () => { + const response = await sendTestRequest( + new Request( + new URL( + `${meta.route}?username=${users[0].data.username}-nonexistent`, + config.http.base_url, + ), + ), + ); + + expect(response.status).toBe(404); + }); +}); diff --git a/server/api/api/v1/accounts/id/index.ts b/server/api/api/v1/accounts/id/index.ts new file mode 100644 index 00000000..69d687ff --- /dev/null +++ b/server/api/api/v1/accounts/id/index.ts @@ -0,0 +1,51 @@ +import { applyConfig, auth, handleZodError } from "@/api"; +import { errorResponse, jsonResponse } from "@/response"; +import type { Hono } from "@hono/hono"; +import { zValidator } from "@hono/zod-validator"; +import { and, eq, isNull } from "drizzle-orm"; +import { z } from "zod"; +import { RolePermissions, Users } from "~/drizzle/schema"; +import { User } from "~/packages/database-interface/user"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + ratelimits: { + max: 30, + duration: 60, + }, + route: "/api/v1/accounts/id", + auth: { + required: false, + oauthPermissions: [], + }, + permissions: { + required: [RolePermissions.Search], + }, +}); + +export const schemas = { + query: z.object({ + username: z.string().min(1).max(512), + }), +}; + +export default (app: Hono) => + app.on( + meta.allowedMethods, + meta.route, + zValidator("query", schemas.query, handleZodError), + auth(meta.auth, meta.permissions), + async (context) => { + const { username } = context.req.valid("query"); + + const user = await User.fromSql( + and(eq(Users.username, username), isNull(Users.instanceId)), + ); + + if (!user) { + return errorResponse("User not found", 404); + } + + return jsonResponse(user.toApi()); + }, + );