server: refactor API error

This commit is contained in:
Johann150 2022-12-19 20:12:24 +01:00
parent c411669133
commit bd68096ea9
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
3 changed files with 33 additions and 40 deletions

View file

@ -12,21 +12,6 @@ export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<vo
? ctx.query
: ctx.request.body;
const error = (e: ApiError): void => {
ctx.status = e.httpStatusCode;
if (e.httpStatusCode === 401) {
ctx.response.set('WWW-Authenticate', 'Bearer');
}
ctx.body = {
error: {
message: e!.message,
code: e!.code,
...(e!.info ? { info: e!.info } : {}),
endpoint: endpoint.name,
},
};
};
// Authentication
// for GET requests, do not even pass on the body parameter as it is considered unsafe
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
@ -43,13 +28,13 @@ export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<vo
ctx.body = typeof res === 'string' ? JSON.stringify(res) : res;
}
}).catch((e: ApiError) => {
error(e);
e.apply(ctx, endpoint.name);
});
}).catch(e => {
if (e instanceof AuthenticationError) {
error(new ApiError('AUTHENTICATION_FAILED', e.message));
new ApiError('AUTHENTICATION_FAILED', e.message).apply(ctx, endpoint.name);
} else {
error(new ApiError());
new ApiError().apply(ctx, endpoint.name);
}
});
}

View file

@ -1,3 +1,5 @@
import Koa from 'koa';
export class ApiError extends Error {
public message: string;
public code: string;
@ -20,6 +22,24 @@ export class ApiError extends Error {
this.message = message;
this.httpStatusCode = httpStatusCode;
}
/**
* Makes the response of ctx the current error, given the respective endpoint name.
*/
public apply(ctx: Koa.Context, endpoint: string): void {
ctx.status = this.httpStatusCode;
if (ctx.status === 401) {
ctx.response.set('WWW-Authenticate', 'Bearer');
}
ctx.body = {
error: {
message: this.message,
code: this.code,
info: this.info ?? undefined,
endpoint,
},
};
}
}
export const errors: Record<string, { message: string, httpStatusCode: number }> = {

View file

@ -12,7 +12,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import signin from '../common/signin.js';
import { verifyLogin, hash } from '../2fa.js';
import { limiter } from '../limiter.js';
import { ApiError } from '../error.js';
import { ApiError, errors } from '../error.js';
export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url);
@ -21,42 +21,30 @@ export default async (ctx: Koa.Context) => {
const body = ctx.request.body as any;
const { username, password, token } = body;
// taken from @server/api/api-handler.ts
function error (e: ApiError): void {
ctx.status = e.httpStatusCode;
if (e.httpStatusCode === 401) {
ctx.response.set('WWW-Authenticate', 'Bearer');
}
ctx.body = {
error: {
message: e!.message,
code: e!.code,
...(e!.info ? { info: e!.info } : {}),
endpoint: 'signin',
},
};
function error(e: keyof errors, info?: Record<string, any>): void {
new ApiError(e, info).apply(ctx, 'signin');
}
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip));
} catch (err) {
error(new ApiError('RATE_LIMIT_EXCEEDED'));
error('RATE_LIMIT_EXCEEDED');
return;
}
if (typeof username !== 'string') {
error(new ApiError('INVALID_PARAM', { param: 'username', reason: 'not a string' }));
error('INVALID_PARAM', { param: 'username', reason: 'not a string' });
return;
}
if (typeof password !== 'string') {
error(new ApiError('INVALID_PARAM', { param: 'password', reason: 'not a string' }));
error('INVALID_PARAM', { param: 'password', reason: 'not a string' });
return;
}
if (token != null && typeof token !== 'string') {
error(new ApiError('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' }));
error('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' });
return;
}
@ -67,12 +55,12 @@ export default async (ctx: Koa.Context) => {
}) as ILocalUser;
if (user == null) {
error(new ApiError('NO_SUCH_USER'));
error('NO_SUCH_USER');
return;
}
if (user.isSuspended) {
error(new ApiError('SUSPENDED'));
error('SUSPENDED');
return;
}
@ -92,7 +80,7 @@ export default async (ctx: Koa.Context) => {
success: false,
});
error(new ApiError('ACCESS_DENIED'));
error('ACCESS_DENIED');
}
if (!profile.twoFactorEnabled) {