From ba76c5e67b008714cce450c9f99e1f0bd1dcca61 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Mon, 11 Dec 2023 23:03:15 +0100 Subject: [PATCH] [backend/web-api] Add basic timeline endpoint --- .../src/server/api/web/controllers/auth.ts | 12 +++--- .../server/api/web/controllers/timeline.ts | 20 +++++++++ .../src/server/api/web/handlers/note.ts | 2 +- .../src/server/api/web/handlers/timeline.ts | 43 +++++++++++++++++++ packages/backend/src/server/api/web/index.ts | 2 + .../src/server/api/web/middleware/auth.ts | 11 +++-- 6 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 packages/backend/src/server/api/web/controllers/timeline.ts create mode 100644 packages/backend/src/server/api/web/handlers/timeline.ts diff --git a/packages/backend/src/server/api/web/controllers/auth.ts b/packages/backend/src/server/api/web/controllers/auth.ts index e54d2360c..8b87f2f31 100644 --- a/packages/backend/src/server/api/web/controllers/auth.ts +++ b/packages/backend/src/server/api/web/controllers/auth.ts @@ -15,7 +15,7 @@ import { secureRndstr } from "@/misc/secure-rndstr.js"; @Controller('/auth') export class AuthController { @Get('/') - async getAuth( + async getAuthStatus( @CurrentUser() me: ILocalUser | null, @CurrentSession() session: Session | null, ): Promise { @@ -30,15 +30,15 @@ export class AuthController { @Post('/') @Flow([RatelimitRouteMiddleware("auth", 10, 60000, true)]) async login(@Body({ required: true }) request: AuthRequest): Promise { - if (request.username == null || request.password == null) throw badRequest(); + if (request.username == null || request.password == null) throw badRequest("Missing username or password"); const user = await Users.findOneBy({ usernameLower: request.username.toLowerCase(), host: IsNull() }); - if (!user) throw unauthorized(); + if (!user) throw unauthorized("Invalid username or password"); const profile = await UserProfiles.findOneBy( { userId: user.id }); - if (!profile || profile.password == null) throw unauthorized(); + if (!profile || profile.password == null) throw unauthorized("Invalid username or password"); - if (!await comparePassword(request.password, profile.password)) throw unauthorized(); + if (!await comparePassword(request.password, profile.password)) throw unauthorized("Invalid username or password"); const result = await Sessions.insert({ id: genId(), @@ -50,6 +50,6 @@ export class AuthController { const session = await Sessions.findOneByOrFail(result.identifiers[0]); - return this.getAuth(user as ILocalUser, session); + return this.getAuthStatus(user as ILocalUser, session); } } diff --git a/packages/backend/src/server/api/web/controllers/timeline.ts b/packages/backend/src/server/api/web/controllers/timeline.ts new file mode 100644 index 000000000..3e1e24df6 --- /dev/null +++ b/packages/backend/src/server/api/web/controllers/timeline.ts @@ -0,0 +1,20 @@ +import { Controller, CurrentUser, Flow, Get, Params, Query } from "@iceshrimp/koa-openapi"; +import { UserResponse } from "@/server/api/web/entities/user.js"; +import { TimelineResponse } from "@/server/api/web/entities/note.js"; +import type { ILocalUser } from "@/models/entities/user.js"; +import { UserHandler } from "@/server/api/web/handlers/user.js"; +import { TimelineHandler } from "@/server/api/web/handlers/timeline.js"; +import { AuthorizationMiddleware } from "@/server/api/web/middleware/auth.js"; + +@Controller('/timeline') +export class TimelineController { + @Get('/home') + @Flow([AuthorizationMiddleware()]) + async getHomeTimeline( + @CurrentUser() me: ILocalUser, + @Query('limit') limit: number = 20, + @Query('replies') replies: boolean = true, + ): Promise { + return TimelineHandler.getHomeTimeline(me, limit, replies); + } +} diff --git a/packages/backend/src/server/api/web/handlers/note.ts b/packages/backend/src/server/api/web/handlers/note.ts index fd508b983..0038d2896 100644 --- a/packages/backend/src/server/api/web/handlers/note.ts +++ b/packages/backend/src/server/api/web/handlers/note.ts @@ -1,7 +1,7 @@ import { ILocalUser } from "@/models/entities/user.js"; import { NoteResponse } from "@/server/api/web/entities/note.js"; import { Notes } from "@/models/index.js"; -import { Boom, notFound, internal } from "@hapi/boom"; +import { Boom, internal } from "@hapi/boom"; import { Note } from "@/models/entities/note.js"; import { UserHandler } from "@/server/api/web/handlers/user.js"; import isQuote from "@/misc/is-quote.js"; diff --git a/packages/backend/src/server/api/web/handlers/timeline.ts b/packages/backend/src/server/api/web/handlers/timeline.ts new file mode 100644 index 000000000..02693cf92 --- /dev/null +++ b/packages/backend/src/server/api/web/handlers/timeline.ts @@ -0,0 +1,43 @@ +import { TimelineResponse } from "@/server/api/web/entities/note.js"; +import { UserResponse } from "@/server/api/web/entities/user.js"; +import { Notes, UserProfiles, Users } from "@/models/index.js"; +import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; +import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; +import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; +import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; +import { ILocalUser, User } from "@/models/entities/user.js"; +import { notFound } from "@hapi/boom"; +import { NoteHandler } from "@/server/api/web/handlers/note.js"; +import { generateFollowingQuery } from "@/server/api/common/generate-following-query.js"; +import { generateListQuery } from "@/server/api/common/generate-list-query.js"; +import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js"; +import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js"; +import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js"; + +export class TimelineHandler { + public static async getHomeTimeline(me: ILocalUser, limit: number, replies: boolean): Promise { + const query = makePaginationQuery(Notes.createQueryBuilder('note')) + .innerJoinAndSelect("note.user", "user") + .leftJoinAndSelect("note.reply", "reply") + .leftJoinAndSelect("note.renote", "renote") + .leftJoinAndSelect("reply.user", "replyUser") + .leftJoinAndSelect("renote.user", "renoteUser"); + + await generateFollowingQuery(query, me); + generateListQuery(query, me); + generateChannelQuery(query, me); + generateRepliesQuery(query, replies, me); + generateVisibilityQuery(query, me); + generateMutedUserQuery(query, me); + generateBlockedUserQuery(query, me); + generateMutedUserRenotesQueryForNotes(query, me); + + query.andWhere("note.visibility != 'hidden'"); + + const result = query.take(Math.min(limit, 100)).getMany(); + return { + notes: await NoteHandler.encodeMany(await result, me), + limit: limit + } + } +} diff --git a/packages/backend/src/server/api/web/index.ts b/packages/backend/src/server/api/web/index.ts index 9ddc2ca59..083fafad5 100644 --- a/packages/backend/src/server/api/web/index.ts +++ b/packages/backend/src/server/api/web/index.ts @@ -8,6 +8,7 @@ import { ErrorHandlingMiddleware } from "@/server/api/web/middleware/error-handl import { AuthController } from "@/server/api/web/controllers/auth.js"; import { NoteController } from "@/server/api/web/controllers/note.js"; import { WebContext, WebRouter } from "@/server/api/web/misc/koa.js"; +import { TimelineController } from "@/server/api/web/controllers/timeline.js"; export class WebAPI { private readonly router: WebRouter; @@ -26,6 +27,7 @@ export class WebAPI { UserController, NoteController, AuthController, + TimelineController, ], flow: [ AuthenticationMiddleware, diff --git a/packages/backend/src/server/api/web/middleware/auth.ts b/packages/backend/src/server/api/web/middleware/auth.ts index 85ac1b63e..fbcbbdbed 100644 --- a/packages/backend/src/server/api/web/middleware/auth.ts +++ b/packages/backend/src/server/api/web/middleware/auth.ts @@ -3,6 +3,7 @@ import { Next } from "koa"; import { Sessions } from "@/models/index.js"; import { Session } from "@/models/entities/session.js"; import { ILocalUser } from "@/models/entities/user.js"; +import { unauthorized } from "@hapi/boom"; export const AuthenticationMiddleware: WebMiddleware = async (ctx: WebContext, next: Next) => { const session = await authenticate(ctx.headers.authorization); @@ -12,13 +13,11 @@ export const AuthenticationMiddleware: WebMiddleware = async (ctx: WebContext, n await next(); } -export function AuthorizationMiddleware(required: boolean, scopes: string[] = []): WebMiddleware { +export function AuthorizationMiddleware(admin: boolean = false): WebMiddleware { return async (ctx: WebContext, next: Next) => { - try { - if (required && !ctx.state.session?.active) { - throw new Error(); //FIXME - } - } catch {} + if (!ctx.state.session?.active || (admin && !ctx.state.session?.user.isAdmin)) { + throw unauthorized("This method requires an authenticated user"); + } await next(); }