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 6f47e0eb..48afa614 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/api/frontend.md b/docs/api/frontend.md index aef3c0ea..d704c212 100644 --- a/docs/api/frontend.md +++ b/docs/api/frontend.md @@ -219,3 +219,21 @@ This request is authenticated with the user's Mastodon API access token. } ``` +## Get User By Username + +Gets a user by their username. + +```http +GET /api/v1/users/id?username=myCoolUser +``` + +### Response + +Returns an account object. + +```ts +// 200 OK +{ + id: string; + // Account object +} \ No newline at end of file diff --git a/docs/api/mastodon.md b/docs/api/mastodon.md index d1047edd..9e633a1e 100644 --- a/docs/api/mastodon.md +++ b/docs/api/mastodon.md @@ -50,12 +50,13 @@ Contains the same extensions as `/api/v1/instance`, except `banner` which uses t (`/api/v1/accounts/:id`, `/api/v1/accounts/verify_credentials`, ...) -An extra attribute has been adding to all returned account objects: +Two extra attributes has been adding to all returned account objects: ```ts { // ... roles: LysandRoles[]; + uri: string; } ``` @@ -63,7 +64,11 @@ An extra attribute has been adding to all returned account objects: An array of roles from [Lysand Roles](./roles.md). -### `/api/v1/accounts/update_credentials` +### `uri` + +The URI of the account's Lysand object (for federation). Similar to Mastodon's `uri` field on notes. + +## `/api/v1/accounts/update_credentials` The `username` parameter can now (optionally) be set to change the user's handle. diff --git a/package.json b/package.json index 33221088..06e7029e 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@inquirer/input": "^2.2.1", "@json2csv/plainjs": "^7.0.6", "@logtape/logtape": "npm:@jsr/logtape__logtape@0.4.2", - "@lysand-org/client": "^0.2.3", + "@lysand-org/client": "^0.2.4", "@lysand-org/federation": "^2.1.1", "@oclif/core": "^4.0.12", "@tufjs/canonical-json": "^2.0.0", diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index ff012b53..e4bb7b29 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -636,6 +636,7 @@ export class User extends BaseInterface { 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()); + }, + );