Compare commits

...

2 Commits

Author SHA1 Message Date
pinks 2665fa1c02 add robots.txt 2023-10-17 15:03:27 +02:00
pinks 083f6bc01c simplify api routes 2023-10-17 15:03:14 +02:00
9 changed files with 92 additions and 147 deletions

9
api/botRoute.ts Normal file
View File

@ -0,0 +1,9 @@
import { createEndpoint, createMethodFilter } from "t_rest/server";
import { bot } from "../bot/mod.ts";
export const botRoute = createMethodFilter({
GET: createEndpoint({ query: null, body: null }, async () => {
const username = bot.botInfo.username;
return { status: 200, body: { type: "application/json", data: { username } } };
}),
});

View File

@ -2,8 +2,7 @@ import { deepMerge } from "std/collections/deep_merge.ts";
import { info } from "std/log/mod.ts"; import { info } from "std/log/mod.ts";
import { createEndpoint, createMethodFilter } from "t_rest/server"; import { createEndpoint, createMethodFilter } from "t_rest/server";
import { configSchema, getConfig, setConfig } from "../app/config.ts"; import { configSchema, getConfig, setConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts"; import { withUser } from "./withUser.ts";
import { sessions } from "./sessionsRoute.ts";
export const paramsRoute = createMethodFilter({ export const paramsRoute = createMethodFilter({
GET: createEndpoint( GET: createEndpoint(
@ -23,23 +22,13 @@ export const paramsRoute = createMethodFilter({
}, },
}, },
async ({ query, body }) => { async ({ query, body }) => {
const session = sessions.get(query.sessionId); return withUser(query, async (chat) => {
if (!session?.userId) { const config = await getConfig();
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } }; info(`User ${chat.username} updated default params: ${JSON.stringify(body.data)}`);
} const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
const chat = await bot.api.getChat(session.userId); await setConfig({ defaultParams });
if (chat.type !== "private") throw new Error("Chat is not private"); return { status: 200, body: { type: "application/json", data: config.defaultParams } };
if (!chat.username) { }, { admin: true });
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
info(`User ${chat.username} updated default params: ${JSON.stringify(body.data)}`);
const defaultParams = deepMerge(config.defaultParams ?? {}, body.data);
await setConfig({ defaultParams });
return { status: 200, body: { type: "application/json", data: config.defaultParams } };
}, },
), ),
}); });

View File

@ -1,16 +1,11 @@
import { import { createLoggerMiddleware, createPathFilter } from "t_rest/server";
createEndpoint, import { botRoute } from "./botRoute.ts";
createLoggerMiddleware,
createMethodFilter,
createPathFilter,
} from "t_rest/server";
import { jobsRoute } from "./jobsRoute.ts"; import { jobsRoute } from "./jobsRoute.ts";
import { paramsRoute } from "./paramsRoute.ts"; import { paramsRoute } from "./paramsRoute.ts";
import { sessionsRoute } from "./sessionsRoute.ts"; import { sessionsRoute } from "./sessionsRoute.ts";
import { statsRoute } from "./statsRoute.ts"; import { statsRoute } from "./statsRoute.ts";
import { usersRoute } from "./usersRoute.ts"; import { usersRoute } from "./usersRoute.ts";
import { workersRoute } from "./workersRoute.ts"; import { workersRoute } from "./workersRoute.ts";
import { bot } from "../bot/mod.ts";
export const serveApi = createLoggerMiddleware( export const serveApi = createLoggerMiddleware(
createPathFilter({ createPathFilter({
@ -20,13 +15,7 @@ export const serveApi = createLoggerMiddleware(
"settings/params": paramsRoute, "settings/params": paramsRoute,
"stats": statsRoute, "stats": statsRoute,
"workers": workersRoute, "workers": workersRoute,
"bot": createMethodFilter({ "bot": botRoute,
// deno-lint-ignore require-await
GET: createEndpoint({ query: null, body: null }, async () => {
const username = bot.botInfo.username;
return { status: 200, body: { type: "application/json", data: { username } } };
}),
}),
}), }),
{ filterStatus: (status) => status >= 400 }, { filterStatus: (status) => status >= 400 },
); );

View File

@ -1,4 +1,3 @@
// deno-lint-ignore-file require-await
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server"; import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { ulid } from "ulid"; import { ulid } from "ulid";

28
api/withUser.ts Normal file
View File

@ -0,0 +1,28 @@
import { Chat } from "grammy_types";
import { Output } from "t_rest/client";
import { getConfig } from "../app/config.ts";
import { bot } from "../bot/mod.ts";
import { sessions } from "./sessionsRoute.ts";
export async function withUser<O extends Output>(
query: { sessionId: string },
cb: (user: Chat.PrivateGetChat) => Promise<O>,
options?: { admin?: boolean },
) {
const session = sessions.get(query.sessionId);
if (!session?.userId) {
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } } as const;
}
const chat = await bot.api.getChat(session.userId);
if (chat.type !== "private") throw new Error("Chat is not private");
if (options?.admin) {
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } } as const;
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } } as const;
}
}
return cb(chat);
}

View File

@ -3,14 +3,16 @@ import { Model } from "indexed_kv";
import createOpenApiFetch from "openapi_fetch"; import createOpenApiFetch from "openapi_fetch";
import { info } from "std/log/mod.ts"; import { info } from "std/log/mod.ts";
import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server"; import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server";
import { getConfig } from "../app/config.ts";
import { activeGenerationWorkers } from "../app/generationQueue.ts"; import { activeGenerationWorkers } from "../app/generationQueue.ts";
import { generationStore } from "../app/generationStore.ts"; import { generationStore } from "../app/generationStore.ts";
import * as SdApi from "../app/sdApi.ts"; import * as SdApi from "../app/sdApi.ts";
import { WorkerInstance, workerInstanceStore } from "../app/workerInstanceStore.ts"; import {
import { bot } from "../bot/mod.ts"; WorkerInstance,
workerInstanceSchema,
workerInstanceStore,
} from "../app/workerInstanceStore.ts";
import { getAuthHeader } from "../utils/getAuthHeader.ts"; import { getAuthHeader } from "../utils/getAuthHeader.ts";
import { sessions } from "./sessionsRoute.ts"; import { withUser } from "./withUser.ts";
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & { export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
id: string; id: string;
@ -69,7 +71,6 @@ export const workersRoute = createPathFilter({
async () => { async () => {
const workerInstances = await workerInstanceStore.getAll(); const workerInstances = await workerInstanceStore.getAll();
const workers = await Promise.all(workerInstances.map(getWorkerData)); const workers = await Promise.all(workerInstances.map(getWorkerData));
return { return {
status: 200, status: 200,
body: { type: "application/json", data: workers satisfies WorkerData[] }, body: { type: "application/json", data: workers satisfies WorkerData[] },
@ -83,51 +84,18 @@ export const workersRoute = createPathFilter({
}, },
body: { body: {
type: "application/json", type: "application/json",
schema: { schema: workerInstanceSchema,
type: "object",
properties: {
key: { type: "string" },
name: { type: ["string", "null"] },
sdUrl: { type: "string" },
sdAuth: {
type: ["object", "null"],
properties: {
user: { type: "string" },
password: { type: "string" },
},
required: ["user", "password"],
},
},
required: ["key", "name", "sdUrl", "sdAuth"],
},
}, },
}, },
async ({ query, body }) => { async ({ query, body }) => {
const session = sessions.get(query.sessionId); return withUser(query, async (chat) => {
if (!session?.userId) { const workerInstance = await workerInstanceStore.create(body.data);
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } }; info(`User ${chat.username} created worker ${workerInstance.id}`);
} return {
const chat = await bot.api.getChat(session.userId); status: 200,
if (chat.type !== "private") throw new Error("Chat is not private"); body: { type: "application/json", data: await getWorkerData(workerInstance) },
if (!chat.username) { };
return { status: 403, body: { type: "text/plain", data: "Must have a username" } }; }, { admin: true });
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
const workerInstance = await workerInstanceStore.create({
key: body.data.key,
name: body.data.name,
sdUrl: body.data.sdUrl,
sdAuth: body.data.sdAuth,
});
info(`User ${chat.username} created worker ${workerInstance.id}`);
const worker = await getWorkerData(workerInstance);
return {
status: 200,
body: { type: "application/json", data: worker satisfies WorkerData },
};
}, },
), ),
}), }),
@ -141,10 +109,9 @@ export const workersRoute = createPathFilter({
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
const worker: WorkerData = await getWorkerData(workerInstance);
return { return {
status: 200, status: 200,
body: { type: "application/json", data: worker satisfies WorkerData }, body: { type: "application/json", data: await getWorkerData(workerInstance) },
}; };
}, },
), ),
@ -155,22 +122,7 @@ export const workersRoute = createPathFilter({
}, },
body: { body: {
type: "application/json", type: "application/json",
schema: { schema: { ...workerInstanceSchema, required: [] },
type: "object",
properties: {
key: { type: "string" },
name: { type: ["string", "null"] },
sdUrl: { type: "string" },
auth: {
type: ["object", "null"],
properties: {
user: { type: "string" },
password: { type: "string" },
},
required: ["user", "password"],
},
},
},
}, },
}, },
async ({ params, query, body }) => { async ({ params, query, body }) => {
@ -178,37 +130,18 @@ export const workersRoute = createPathFilter({
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
const session = sessions.get(query.sessionId); return withUser(query, async (chat) => {
if (!session?.userId) { info(
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } }; `User ${chat.username} updated worker ${params.workerId}: ${
} JSON.stringify(body.data)
const chat = await bot.api.getChat(session.userId); }`,
if (chat.type !== "private") throw new Error("Chat is not private"); );
if (!chat.username) { await workerInstance.update(body.data);
return { status: 403, body: { type: "text/plain", data: "Must have a username" } }; return {
} status: 200,
const config = await getConfig(); body: { type: "application/json", data: await getWorkerData(workerInstance) },
if (!config?.adminUsernames?.includes(chat.username)) { };
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } }; }, { admin: true });
}
if (body.data.name !== undefined) {
workerInstance.value.name = body.data.name;
}
if (body.data.sdUrl !== undefined) {
workerInstance.value.sdUrl = body.data.sdUrl;
}
if (body.data.auth !== undefined) {
workerInstance.value.sdAuth = body.data.auth;
}
info(
`User ${chat.username} updated worker ${params.workerId}: ${JSON.stringify(body.data)}`,
);
await workerInstance.update();
const worker = await getWorkerData(workerInstance);
return {
status: 200,
body: { type: "application/json", data: worker satisfies WorkerData },
};
}, },
), ),
DELETE: createEndpoint( DELETE: createEndpoint(
@ -223,22 +156,11 @@ export const workersRoute = createPathFilter({
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
const session = sessions.get(query.sessionId); return withUser(query, async (chat) => {
if (!session?.userId) { info(`User ${chat.username} deleted worker ${params.workerId}`);
return { status: 401, body: { type: "text/plain", data: "Must be logged in" } }; await workerInstance.delete();
} return { status: 200, body: { type: "application/json", data: null } };
const chat = await bot.api.getChat(session.userId); }, { admin: true });
if (chat.type !== "private") throw new Error("Chat is not private");
if (!chat.username) {
return { status: 403, body: { type: "text/plain", data: "Must have a username" } };
}
const config = await getConfig();
if (!config?.adminUsernames?.includes(chat.username)) {
return { status: 403, body: { type: "text/plain", data: "Must be an admin" } };
}
info(`User ${chat.username} deleted worker ${params.workerId}`);
await workerInstance.delete();
return { status: 200, body: { type: "application/json", data: null } };
}, },
), ),
}), }),

View File

@ -9,6 +9,13 @@
"fmt": { "fmt": {
"lineWidth": 100 "lineWidth": 100
}, },
"lint": {
"rules": {
"exclude": [
"require-await"
]
}
},
"imports": { "imports": {
"@date-fns/utc": "https://cdn.skypack.dev/@date-fns/utc@1.1.0?dts", "@date-fns/utc": "https://cdn.skypack.dev/@date-fns/utc@1.1.0?dts",
"async": "https://deno.land/x/async@v2.0.2/mod.ts", "async": "https://deno.land/x/async@v2.0.2/mod.ts",

View File

@ -249,7 +249,7 @@ function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
body: { body: {
type: "application/json", type: "application/json",
data: { data: {
auth: user && password ? { user, password } : null, sdAuth: user && password ? { user, password } : null,
}, },
}, },
}); });

2
ui/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /