mirror of
https://iceshrimp.dev/crimekillz/trashposs
synced 2024-11-24 01:39:06 +01:00
[backend/web-api] Add basic auth endpoints and a bunch of other things
This commit is contained in:
parent
b9c86d0d4c
commit
1870dc33b5
@ -76,6 +76,7 @@ import { OAuthApp } from "@/models/entities/oauth-app.js";
|
|||||||
import { OAuthToken } from "@/models/entities/oauth-token.js";
|
import { OAuthToken } from "@/models/entities/oauth-token.js";
|
||||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||||
|
import { Session } from "@/models/entities/session.js";
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
||||||
class MyCustomLogger implements Logger {
|
class MyCustomLogger implements Logger {
|
||||||
@ -179,6 +180,7 @@ export const entities = [
|
|||||||
OAuthToken,
|
OAuthToken,
|
||||||
HtmlNoteCacheEntry,
|
HtmlNoteCacheEntry,
|
||||||
HtmlUserCacheEntry,
|
HtmlUserCacheEntry,
|
||||||
|
Session,
|
||||||
...charts,
|
...charts,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddSessionTable1702326649645 implements MigrationInterface {
|
||||||
|
name = 'AddSessionTable1702326649645'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_0ee0c7254e5612a8129251997e"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "session" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "token" character varying(64) NOT NULL, "active" boolean NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id")); COMMENT ON COLUMN "session"."createdAt" IS 'The created date of the OAuth token'; COMMENT ON COLUMN "session"."token" IS 'The authorization token'; COMMENT ON COLUMN "session"."active" IS 'Whether or not the token has been activated (i.e. 2fa has been confirmed)'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_232f8e85d7633bd6ddfad42169" ON "session" ("token") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "mastoId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "session" ADD CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "session" DROP CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ADD "mastoId" SERIAL NOT NULL`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_232f8e85d7633bd6ddfad42169"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "session"`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0ee0c7254e5612a8129251997e" ON "notification" ("mastoId") `);
|
||||||
|
}
|
||||||
|
}
|
36
packages/backend/src/models/entities/session.ts
Normal file
36
packages/backend/src/models/entities/session.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column, Index, ManyToOne, JoinColumn } from "typeorm";
|
||||||
|
import { id } from "../id.js";
|
||||||
|
import { OAuthApp } from "@/models/entities/oauth-app.js";
|
||||||
|
import { User } from "@/models/entities/user.js";
|
||||||
|
|
||||||
|
@Entity('session')
|
||||||
|
export class Session {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column("timestamp with time zone", {
|
||||||
|
comment: "The created date of the OAuth token",
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public userId: User["id"];
|
||||||
|
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column("varchar", {
|
||||||
|
length: 64,
|
||||||
|
comment: "The authorization token",
|
||||||
|
})
|
||||||
|
public token: string;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
comment: "Whether or not the token has been activated (i.e. 2fa has been confirmed)",
|
||||||
|
})
|
||||||
|
public active: boolean;
|
||||||
|
}
|
@ -70,6 +70,7 @@ import { OAuthToken } from "@/models/entities/oauth-token.js";
|
|||||||
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
|
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
|
||||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||||
|
import { Session } from "@/models/entities/session.js";
|
||||||
|
|
||||||
export const Announcements = db.getRepository(Announcement);
|
export const Announcements = db.getRepository(Announcement);
|
||||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||||
@ -138,3 +139,4 @@ export const OAuthApps = db.getRepository(OAuthApp);
|
|||||||
export const OAuthTokens = db.getRepository(OAuthToken);
|
export const OAuthTokens = db.getRepository(OAuthToken);
|
||||||
export const HtmlUserCacheEntries = db.getRepository(HtmlUserCacheEntry);
|
export const HtmlUserCacheEntries = db.getRepository(HtmlUserCacheEntry);
|
||||||
export const HtmlNoteCacheEntries = db.getRepository(HtmlNoteCacheEntry);
|
export const HtmlNoteCacheEntries = db.getRepository(HtmlNoteCacheEntry);
|
||||||
|
export const Sessions = db.getRepository(Session);
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { Controller, Get, CurrentUser, Params, } from "@iceshrimp/koa-openapi";
|
|
||||||
import type { ILocalUser } from "@/models/entities/user.js";
|
|
||||||
import { NoteHandler } from "@/server/api/web/handlers/note.js";
|
|
||||||
|
|
||||||
@Controller('/note')
|
|
||||||
export class NoteController {
|
|
||||||
@Get('/:id')
|
|
||||||
async getNote(
|
|
||||||
@CurrentUser() me: ILocalUser | null,
|
|
||||||
@Params('id') id: string,
|
|
||||||
) {
|
|
||||||
NoteHandler.getNoteOrFail(me, id);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,56 @@
|
|||||||
import { Controller, CurrentUser, Get } from "@iceshrimp/koa-openapi";
|
import { Controller, Get, Post, Body, CurrentUser, Flow } from "@iceshrimp/koa-openapi";
|
||||||
import type { ILocalUser } from "@/models/entities/user.js";
|
import type { ILocalUser } from "@/models/entities/user.js";
|
||||||
import { UserHandler } from "@/server/api/web/handlers/user.js";
|
import { UserHandler } from "@/server/api/web/handlers/user.js";
|
||||||
import { AuthResponse } from "@/server/api/web/entities/auth.js";
|
import type { AuthRequest, AuthResponse } from "@/server/api/web/entities/auth.js";
|
||||||
|
import type { Session } from "@/models/entities/session.js";
|
||||||
|
import { RatelimitRouteMiddleware } from "@/server/api/web/middleware/rate-limit.js";
|
||||||
|
import { CurrentSession } from "@/server/api/web/misc/decorators.js";
|
||||||
|
import { Sessions, UserProfiles, Users } from "@/models/index.js";
|
||||||
|
import { unauthorized, badRequest } from "@hapi/boom";
|
||||||
|
import { comparePassword } from "@/misc/password.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import { secureRndstr } from "@/misc/secure-rndstr.js";
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getAuth(
|
async getAuth(
|
||||||
@CurrentUser() me: ILocalUser | null,
|
@CurrentUser() me: ILocalUser | null,
|
||||||
|
@CurrentSession() session: Session | null,
|
||||||
): Promise<AuthResponse> {
|
): Promise<AuthResponse> {
|
||||||
const user = me ? await UserHandler.getUser(me, me.id) : null;
|
const user = me ? await UserHandler.getUser(me, me.id) : null;
|
||||||
return {
|
return {
|
||||||
authenticated: !!me,
|
authenticated: !!session?.active,
|
||||||
|
status: user && session?.active ? null : '2fa',
|
||||||
|
token: session?.token ?? null,
|
||||||
user: user,
|
user: user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
|
||||||
|
const user = await Users.findOneBy({ usernameLower: request.username.toLowerCase(), host: IsNull() });
|
||||||
|
if (!user) throw unauthorized();
|
||||||
|
|
||||||
|
const profile = await UserProfiles.findOneBy( { userId: user.id });
|
||||||
|
if (!profile || profile.password == null) throw unauthorized();
|
||||||
|
|
||||||
|
if (!await comparePassword(request.password, profile.password)) throw unauthorized();
|
||||||
|
|
||||||
|
const result = await Sessions.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
active: !profile.twoFactorEnabled,
|
||||||
|
userId: user.id,
|
||||||
|
token: secureRndstr(32),
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await Sessions.findOneByOrFail(result.identifiers[0]);
|
||||||
|
|
||||||
|
return this.getAuth(user as ILocalUser, session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,12 @@ import { UserResponse } from "@/server/api/web/entities/user.js";
|
|||||||
|
|
||||||
export type AuthResponse = {
|
export type AuthResponse = {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
status: null | '2fa';
|
||||||
|
token: string | null;
|
||||||
user: UserResponse | null;
|
user: UserResponse | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthRequest = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
@ -6,10 +6,11 @@ export type NoteResponse = {
|
|||||||
text: string | null;
|
text: string | null;
|
||||||
user: UserResponse;
|
user: UserResponse;
|
||||||
reply: NoteResponse | undefined | null; // Undefined if no record, null if not visible
|
reply: NoteResponse | undefined | null; // Undefined if no record, null if not visible
|
||||||
renote: NoteResponse | undefined | null; // Undefined if no record, null if not visible
|
renote: NoteResponse | undefined | null; // Undefined if no record, null if not visible
|
||||||
|
quote: NoteResponse | undefined | null; // Undefined if no record, null if not visible
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TimelineResponse = {
|
export type TimelineResponse = {
|
||||||
notes: NoteResponse[];
|
notes: NoteResponse[];
|
||||||
pagination: {}; //TODO
|
limit: number;
|
||||||
};
|
};
|
||||||
|
@ -24,7 +24,8 @@ export class NoteHandler {
|
|||||||
id: note.id,
|
id: note.id,
|
||||||
text: note.text,
|
text: note.text,
|
||||||
user: note.user ? await UserHandler.encode(note.user, me) : await UserHandler.getUser(me, note.userId),
|
user: note.user ? await UserHandler.encode(note.user, me) : await UserHandler.getUser(me, note.userId),
|
||||||
renote: note.renoteId && recurse > 0 ? await this.encode(note.renote ?? await this.getNoteOrFail(note.renoteId), me, isQuote(note) ? --recurse : 0) : undefined,
|
renote: !isQuote(note) && note.renoteId && recurse > 0 ? await this.encode(note.renote ?? await this.getNoteOrFail(note.renoteId), me, 0) : undefined,
|
||||||
|
quote: isQuote(note) && note.renoteId && recurse > 0 ? await this.encode(note.renote ?? await this.getNoteOrFail(note.renoteId), me, --recurse) : undefined,
|
||||||
reply: note.replyId && recurse > 0 ? await this.encode(note.renote ?? await this.getNoteOrFail(note.replyId), me, 0) : undefined,
|
reply: note.replyId && recurse > 0 ? await this.encode(note.renote ?? await this.getNoteOrFail(note.replyId), me, 0) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export class UserHandler {
|
|||||||
const result = query.take(Math.min(limit, 100)).getMany();
|
const result = query.take(Math.min(limit, 100)).getMany();
|
||||||
return {
|
return {
|
||||||
notes: await NoteHandler.encodeMany(await result, me),
|
notes: await NoteHandler.encodeMany(await result, me),
|
||||||
pagination: {},
|
limit: limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import Koa, { DefaultState, Context, Middleware } from "koa";
|
import Koa, { DefaultState } from "koa";
|
||||||
import { bootstrapControllers, Ctx } from "@iceshrimp/koa-openapi";
|
import { bootstrapControllers } from "@iceshrimp/koa-openapi";
|
||||||
import { ILocalUser } from "@/models/entities/user.js";
|
|
||||||
import { AccessToken } from "@/models/entities/access-token.js";
|
|
||||||
import { UserController } from "@/server/api/web/controllers/user.js";
|
import { UserController } from "@/server/api/web/controllers/user.js";
|
||||||
import { RatelimitMiddleware } from "@/server/api/web/middleware/rate-limit.js";
|
import { RatelimitMiddleware } from "@/server/api/web/middleware/rate-limit.js";
|
||||||
import { AuthenticationMiddleware } from "@/server/api/web/middleware/auth.js";
|
import { AuthenticationMiddleware } from "@/server/api/web/middleware/auth.js";
|
||||||
import { ErrorHandlingMiddleware } from "@/server/api/web/middleware/error-handling.js";
|
import { ErrorHandlingMiddleware } from "@/server/api/web/middleware/error-handling.js";
|
||||||
import { AuthController } from "@/server/api/web/controllers/auth.js";
|
import { AuthController } from "@/server/api/web/controllers/auth.js";
|
||||||
import { NoteController } from "@/server/api/web/controllers/note.js";
|
import { NoteController } from "@/server/api/web/controllers/note.js";
|
||||||
|
import { WebContext, WebRouter } from "@/server/api/web/misc/koa.js";
|
||||||
export type WebRouter = Router<WebState, WebContext>;
|
|
||||||
export type WebMiddleware = Middleware<WebState, WebContext>;
|
|
||||||
|
|
||||||
export interface WebState extends DefaultState {}
|
|
||||||
|
|
||||||
export interface WebContext extends Context {
|
|
||||||
user: ILocalUser | null;
|
|
||||||
token: AccessToken | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebAPI {
|
export class WebAPI {
|
||||||
private readonly router: WebRouter;
|
private readonly router: WebRouter;
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import { WebMiddleware, WebContext, WebState } from "@/server/api/web/index.js";
|
import { WebContext, WebMiddleware } from "@/server/api/web/misc/koa.js";
|
||||||
import { Next } from "koa";
|
import { Next } from "koa";
|
||||||
import authenticate from "@/server/api/authenticate.js";
|
import { Sessions } from "@/models/index.js";
|
||||||
|
import { Session } from "@/models/entities/session.js";
|
||||||
|
import { ILocalUser } from "@/models/entities/user.js";
|
||||||
|
|
||||||
export const AuthenticationMiddleware: WebMiddleware = async (ctx: WebContext, next: Next) => {
|
export const AuthenticationMiddleware: WebMiddleware = async (ctx: WebContext, next: Next) => {
|
||||||
try {
|
const session = await authenticate(ctx.headers.authorization);
|
||||||
const [ user, token ] = await authenticate(ctx.headers.authorization, null, false);
|
ctx.state.user = session?.user as ILocalUser;
|
||||||
|
ctx.state.session = session;
|
||||||
//FIXME we shouldn't need to cast this
|
|
||||||
(ctx.state as WebState).user = user ?? null;
|
|
||||||
(ctx.state as WebState).token = token ?? null;
|
|
||||||
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
@ -18,7 +15,7 @@ export const AuthenticationMiddleware: WebMiddleware = async (ctx: WebContext, n
|
|||||||
export function AuthorizationMiddleware(required: boolean, scopes: string[] = []): WebMiddleware {
|
export function AuthorizationMiddleware(required: boolean, scopes: string[] = []): WebMiddleware {
|
||||||
return async (ctx: WebContext, next: Next) => {
|
return async (ctx: WebContext, next: Next) => {
|
||||||
try {
|
try {
|
||||||
if (required && !(ctx.state as WebState).user) {
|
if (required && !ctx.state.session?.active) {
|
||||||
throw new Error(); //FIXME
|
throw new Error(); //FIXME
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@ -26,3 +23,10 @@ export function AuthorizationMiddleware(required: boolean, scopes: string[] = []
|
|||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function authenticate(token: string | undefined): Promise<Session | null> {
|
||||||
|
if (token == null || token.length < 1) return null;
|
||||||
|
if (token.toLowerCase().startsWith('bearer ')) token = token.substring(7);
|
||||||
|
|
||||||
|
return Sessions.findOne({ where: { token }, relations: ["user"] });
|
||||||
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import koaRatelimit from "koa-ratelimit";
|
import koaRatelimit from "koa-ratelimit";
|
||||||
import { WebContext, WebMiddleware } from "@/server/api/web/index.js";
|
import { WebContext, WebMiddleware } from "@/server/api/web/misc/koa.js";
|
||||||
import { Next } from "koa";
|
import { Next } from "koa";
|
||||||
import { redisClient } from "@/db/redis.js";
|
import { redisClient } from "@/db/redis.js";
|
||||||
import { tooManyRequests } from "@hapi/boom";
|
import { tooManyRequests } from "@hapi/boom";
|
||||||
|
|
||||||
export const RatelimitMiddleware: WebMiddleware = async (ctx: WebContext, next: Next) => {
|
export async function RatelimitMiddleware(ctx: WebContext, next: Next) {
|
||||||
// We can't assign limiter directly if we want to preserve type hints for WebContext and WebState
|
// We can't assign limiter directly if we want to preserve type hints for WebContext and WebState
|
||||||
//TODO: server config options (disable limiter entirely, set max/duration, set different rate limits for auth/noauth, bypass rate limit for admins)
|
//TODO: server config options (disable limiter entirely, set max/duration, set different rate limits for auth/noauth, bypass rate limit for admins)
|
||||||
const limiter = koaRatelimit({
|
const limiter = koaRatelimit({
|
||||||
@ -23,10 +23,36 @@ export const RatelimitMiddleware: WebMiddleware = async (ctx: WebContext, next:
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await limiter(ctx, next);
|
await limiter(ctx, next);
|
||||||
}
|
} catch (e: any) {
|
||||||
catch (e: any) {
|
|
||||||
if (e.name === 'TooManyRequestsError')
|
if (e.name === 'TooManyRequestsError')
|
||||||
throw tooManyRequests(e.message);
|
throw tooManyRequests(e.message);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export function RatelimitRouteMiddleware(prefix: string, max: number = 500, duration: number = 60000, ipOnly: boolean = false): WebMiddleware {
|
||||||
|
return async (ctx: WebContext, next: Next) => {
|
||||||
|
const limiter = koaRatelimit({
|
||||||
|
driver: "redis",
|
||||||
|
db: redisClient,
|
||||||
|
max: max,
|
||||||
|
duration: duration,
|
||||||
|
id: () => `${prefix}-${ipOnly ? ctx.request.ip : ctx.state.user?.id ?? ctx.request.ip}`,
|
||||||
|
headers: {
|
||||||
|
remaining: 'X-RateLimit-Remaining',
|
||||||
|
total: 'X-RateLimit-Limit',
|
||||||
|
reset: 'X-RateLimit-Reset',
|
||||||
|
},
|
||||||
|
throw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await limiter(ctx, next);
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
if (e.name === 'TooManyRequestsError')
|
||||||
|
throw tooManyRequests(e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3
packages/backend/src/server/api/web/misc/decorators.ts
Normal file
3
packages/backend/src/server/api/web/misc/decorators.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { State } from "@iceshrimp/koa-openapi";
|
||||||
|
|
||||||
|
export const CurrentSession = ()=>State('session');
|
16
packages/backend/src/server/api/web/misc/koa.ts
Normal file
16
packages/backend/src/server/api/web/misc/koa.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ILocalUser } from "@/models/entities/user.js";
|
||||||
|
import { Session } from "@/models/entities/session.js";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { Context, DefaultState, Middleware } from "koa";
|
||||||
|
|
||||||
|
export type WebRouter = Router<WebState, WebContext>;
|
||||||
|
export type WebMiddleware = Middleware<WebState, WebContext>;
|
||||||
|
|
||||||
|
export interface WebState extends DefaultState {
|
||||||
|
user: ILocalUser | null;
|
||||||
|
session: Session | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebContext extends Context {
|
||||||
|
state: WebState;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user