[backend/web-api] Add basic timeline endpoint

This commit is contained in:
Laura Hausmann 2023-12-11 23:03:15 +01:00
parent 8134e92284
commit ba76c5e67b
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
6 changed files with 77 additions and 13 deletions

View file

@ -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<AuthResponse> {
@ -30,15 +30,15 @@ export class AuthController {
@Post('/')
@Flow([RatelimitRouteMiddleware("auth", 10, 60000, true)])
async login(@Body({ required: true }) request: AuthRequest): Promise<AuthResponse> {
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);
}
}

View file

@ -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<TimelineResponse> {
return TimelineHandler.getHomeTimeline(me, limit, replies);
}
}

View file

@ -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";

View file

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

View file

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

View file

@ -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
if (!ctx.state.session?.active || (admin && !ctx.state.session?.user.isAdmin)) {
throw unauthorized("This method requires an authenticated user");
}
} catch {}
await next();
}