feat(api): Add new endpoint to get a user by its username

This commit is contained in:
Jesse Wierzbinski 2024-07-17 14:02:29 +02:00
parent 407eb5e205
commit be881f18cd
No known key found for this signature in database
9 changed files with 124 additions and 3 deletions

View file

@ -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 [**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 [**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 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 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. - 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. - Refactor logging system to be more robust and easier to use. Logfiles are now automatically rotated.

BIN
bun.lockb

Binary file not shown.

View file

@ -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
}

View file

@ -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`, ...) (`/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 ```ts
{ {
// ... // ...
roles: LysandRoles[]; 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). 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. The `username` parameter can now (optionally) be set to change the user's handle.

View file

@ -103,7 +103,7 @@
"@inquirer/input": "^2.2.1", "@inquirer/input": "^2.2.1",
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@logtape/logtape": "npm:@jsr/logtape__logtape@0.4.2", "@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", "@lysand-org/federation": "^2.1.1",
"@oclif/core": "^4.0.12", "@oclif/core": "^4.0.12",
"@tufjs/canonical-json": "^2.0.0", "@tufjs/canonical-json": "^2.0.0",

View file

@ -636,6 +636,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
username: user.username, username: user.username,
display_name: user.displayName, display_name: user.displayName,
note: user.note, note: user.note,
uri: this.getUri(),
url: url:
user.uri || user.uri ||
new URL(`/@${user.username}`, config.http.base_url).toString(), new URL(`/@${user.username}`, config.http.base_url).toString(),

View file

@ -72,6 +72,7 @@ describe(meta.route, () => {
statuses_count: 40, statuses_count: 40,
note: users[0].data.note, note: users[0].data.note,
acct: users[0].data.username, acct: users[0].data.username,
uri: expect.any(String),
url: expect.any(String), url: expect.any(String),
avatar_static: expect.any(String), avatar_static: expect.any(String),
header_static: expect.any(String), header_static: expect.any(String),

View file

@ -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);
});
});

View file

@ -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());
},
);