2023-01-13 05:40:33 +01:00
|
|
|
import type Koa from "koa";
|
2016-12-28 23:49:51 +01:00
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
import type { User } from "@/models/entities/user.js";
|
|
|
|
import { UserIps } from "@/models/index.js";
|
|
|
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
|
|
|
import type { IEndpoint } from "./endpoints.js";
|
|
|
|
import authenticate, { AuthenticationError } from "./authenticate.js";
|
|
|
|
import call from "./call.js";
|
|
|
|
import { ApiError } from "./error.js";
|
2016-12-28 23:49:51 +01:00
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
const userIpHistories = new Map<User["id"], Set<string>>();
|
2022-07-02 08:12:11 +02:00
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
userIpHistories.clear();
|
|
|
|
}, 1000 * 60 * 60);
|
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
export default (endpoint: IEndpoint, ctx: Koa.Context) =>
|
|
|
|
new Promise<void>((res) => {
|
|
|
|
const body = ctx.is("multipart/form-data")
|
|
|
|
? (ctx.request as any).body
|
|
|
|
: ctx.method === "GET"
|
2022-06-25 11:26:31 +02:00
|
|
|
? ctx.query
|
|
|
|
: ctx.request.body;
|
2018-04-13 04:44:39 +02:00
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
const reply = (x?: any, y?: ApiError) => {
|
|
|
|
if (x == null) {
|
|
|
|
ctx.status = 204;
|
|
|
|
} else if (typeof x === "number" && y) {
|
|
|
|
ctx.status = x;
|
|
|
|
ctx.body = {
|
|
|
|
error: {
|
|
|
|
message: y!.message,
|
|
|
|
code: y!.code,
|
|
|
|
id: y!.id,
|
|
|
|
kind: y!.kind,
|
2023-07-21 21:32:39 +02:00
|
|
|
...((y!.info && process.env.NODE_ENV !== "production") ? { info: y!.info } : {}),
|
2023-01-13 05:40:33 +01:00
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
|
|
|
ctx.body = typeof x === "string" ? JSON.stringify(x) : x;
|
2022-06-25 11:26:31 +02:00
|
|
|
}
|
2023-01-13 05:40:33 +01:00
|
|
|
res();
|
|
|
|
};
|
|
|
|
|
|
|
|
// Authentication
|
|
|
|
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
|
|
|
authenticate(
|
|
|
|
ctx.headers.authorization,
|
|
|
|
ctx.method === "GET" ? null : body["i"],
|
|
|
|
)
|
|
|
|
.then(([user, app]) => {
|
|
|
|
// API invoking
|
|
|
|
call(endpoint.name, user, app, body, ctx)
|
|
|
|
.then((res: any) => {
|
|
|
|
if (
|
|
|
|
ctx.method === "GET" &&
|
|
|
|
endpoint.meta.cacheSec &&
|
|
|
|
!body["i"] &&
|
|
|
|
!user
|
|
|
|
) {
|
|
|
|
ctx.set(
|
|
|
|
"Cache-Control",
|
|
|
|
`public, max-age=${endpoint.meta.cacheSec}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
reply(res);
|
|
|
|
})
|
|
|
|
.catch((e: ApiError) => {
|
|
|
|
reply(
|
|
|
|
e.httpStatusCode
|
|
|
|
? e.httpStatusCode
|
|
|
|
: e.kind === "client"
|
|
|
|
? 400
|
|
|
|
: 500,
|
|
|
|
e,
|
|
|
|
);
|
|
|
|
});
|
2022-07-02 08:12:11 +02:00
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
// Log IP
|
|
|
|
if (user) {
|
|
|
|
fetchMeta().then((meta) => {
|
|
|
|
if (!meta.enableIpLogging) return;
|
|
|
|
const ip = ctx.ip;
|
|
|
|
const ips = userIpHistories.get(user.id);
|
|
|
|
if (ips == null || !ips.has(ip)) {
|
|
|
|
if (ips == null) {
|
|
|
|
userIpHistories.set(user.id, new Set([ip]));
|
|
|
|
} else {
|
|
|
|
ips.add(ip);
|
|
|
|
}
|
2022-07-02 08:12:11 +02:00
|
|
|
|
2023-01-13 05:40:33 +01:00
|
|
|
try {
|
|
|
|
UserIps.createQueryBuilder()
|
|
|
|
.insert()
|
|
|
|
.values({
|
|
|
|
createdAt: new Date(),
|
|
|
|
userId: user.id,
|
|
|
|
ip: ip,
|
|
|
|
})
|
|
|
|
.orIgnore(true)
|
|
|
|
.execute();
|
|
|
|
} catch {}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
if (e instanceof AuthenticationError) {
|
|
|
|
ctx.response.status = 403;
|
|
|
|
ctx.response.set("WWW-Authenticate", "Bearer");
|
|
|
|
ctx.response.body = {
|
|
|
|
message: `Authentication failed: ${e.message}`,
|
|
|
|
code: "AUTHENTICATION_FAILED",
|
|
|
|
id: "b0a7f5f8-dc2f-4171-b91f-de88ad238e14",
|
|
|
|
kind: "client",
|
|
|
|
};
|
|
|
|
res();
|
|
|
|
} else {
|
|
|
|
reply(500, new ApiError());
|
2022-07-02 08:12:11 +02:00
|
|
|
}
|
|
|
|
});
|
2019-02-22 06:46:49 +01:00
|
|
|
});
|