diff --git a/.env.example b/.env.example index 4e9dab6b9..6e13d0951 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ ORDO_ID_ALGORITHM=ECDSA:256 -ORDO_ID_ALLOW_ORIGIN=${ORDO_DT_HOST}, ${ORDO_WEB_HOST} -ORDO_ID_AUD=${ORDO_ID_ALLOW_ORIGIN} +ORDO_ID_ALLOW_ORIGIN=http://localhost:3002, http://localhost:3000 +ORDO_ID_AUD=http://localhost:3002, http://localhost:3000 ORDO_ID_DEFAULT_FILE_LIMIT=1000 ORDO_ID_DEFAULT_MAX_FUNCTIONS=10 ORDO_ID_DEFAULT_MAX_UPLOAD_SIZE=1.5 -ORDO_ID_HOST=http://localhost:3000 -ORDO_ID_ISS=${ORDO_ID_HOST} +ORDO_ID_HOST=http://localhost:3001 +ORDO_ID_ISS=http://localhost:3001 ORDO_ID_PERSISTED_TOKEN_LIFETIME=2592000 ORDO_ID_PORT=3000 ORDO_ID_TOKEN_DB_PATH="var/srv/id/tokens.json" @@ -14,45 +14,14 @@ ORDO_ID_TOKEN_PRIVATE_KEY= ORDO_ID_TOKEN_PUBLIC_KEY= ORDO_ID_USER_DB_PATH="var/srv/id/users.json" -ORDO_ID_USER_TABLE_NAME=users -ORDO_ID_TOKENS_TABLE_NAME=tokens -ORDO_ID_EMAIL_API_KEY= -ORDO_ID_USER_DYNAMODB_ENDPOINT= -ORDO_ID_USER_DYNAMODB_ACCESS_KEY= -ORDO_ID_USER_DYNAMODB_SECRET_KEY= -ORDO_ID_USER_DYNAMODB_REGION= -ORDO_ID_USER_DYNAMODB_USER_TABLE= -ORDO_ID_TOKEN_DYNAMODB_ENDPOINT= -ORDO_ID_TOKEN_DYNAMODB_ACCESS_KEY= -ORDO_ID_TOKEN_DYNAMODB_SECRET_KEY= -ORDO_ID_TOKEN_DYNAMODB_REGION= -ORDO_ID_TOKEN_DYNAMODB_USER_TABLE= - -ORDO_DT_DATA_PERSISTENCE_STRATEGY=fs -ORDO_DT_CONTENT_PERSISTENCE_STRATEGY=fs -ORDO_DT_DATA_PATH=./var/srv/data/data -ORDO_DT_CONTENT_PATH=./var/srv/data/content ORDO_DT_PORT=3002 -ORDO_DT_HOST=http://localhost:${ORDO_DT_PORT} -ORDO_DT_DATA_S3_ACCESS_KEY= -ORDO_DT_DATA_S3_SECRET_KEY= -ORDO_DT_DATA_S3_REGION= -ORDO_DT_DATA_S3_BUCKET_NAME= -ORDO_DT_DATA_S3_ENDPOINT= -ORDO_DT_CONTENT_S3_ACCESS_KEY= -ORDO_DT_CONTENT_S3_SECRET_KEY= -ORDO_DT_CONTENT_S3_REGION= -ORDO_DT_CONTENT_S3_BUCKET_NAME= -ORDO_DT_CONTENT_S3_ENDPOINT= -ORDO_DT_ENV=development -ORDO_DT_REGION=global -ORDO_DT_DOCKER_REGISTRY=ordo.pink -ORDO_DT_DOCKER_REGISTRY_SCOPE=global -ORDO_DT_DOCKER_REGISTRY_USERNAME= -ORDO_DT_DOCKER_REGISTRY_PASSWORD= -ORDO_DT_VERSION=0.1.0 -ORDO_WORKSPACE_PORT=3004 -ORDO_WORKSPACE_HOST=http://localhost:${ORDO_WORKSPACE_PORT} -ORDO_STATIC_PORT=3003 -ORDO_STATIC_HOST=http://localhost:${ORDO_STATIC_PORT} -ORDO_STATIC_ROOT=./var/srv/static +ORDO_ID_HOST=http://localhost:3002 +ORDO_DT_ALLOW_ORIGIN=http://localhost:3000 +ORDO_DT_DATA_PATH=var/srv/dt + +ORDO_PB_PORT=3003 +ORDO_PB_HOST=http://localhost:3003 +ORDO_PB_DATA_PATH=var/srv/pb + +ORDO_WEB_PORT=3000 +ORDO_WEB_HOST=http://localhost:3000 diff --git a/bun.lockb b/bun.lockb index 3833d0e1f..b130e5b8b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lib/backend-dt/index.ts b/lib/backend-dt/index.ts new file mode 100644 index 000000000..2d90b469e --- /dev/null +++ b/lib/backend-dt/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/backend-dt.impl" +export * from "./src/backend-dt.types" diff --git a/lib/backend-dt/license b/lib/backend-dt/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/backend-dt/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/backend-dt/readme.md b/lib/backend-dt/readme.md new file mode 100644 index 000000000..1022d903f --- /dev/null +++ b/lib/backend-dt/readme.md @@ -0,0 +1 @@ +# Backend Dt diff --git a/lib/backend-dt/src/backend-dt.impl.ts b/lib/backend-dt/src/backend-dt.impl.ts new file mode 100644 index 000000000..f645c7d7f --- /dev/null +++ b/lib/backend-dt/src/backend-dt.impl.ts @@ -0,0 +1,219 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { CurrentUser, METADATA_CONTENT_FSID, Metadata, RRR } from "@ordo-pink/core" +import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { Routary, TIntake } from "@ordo-pink/routary" +import { create_json_response, create_response, status_from_rrr } from "@ordo-pink/backend-util-create-response" +import { set_x_response_time_header, start_response_timer, stop_response_timer } from "@ordo-pink/backend-util-response-time" +import { default_handler } from "@ordo-pink/backend-util-default-handler" +import { extract_request_ip } from "@ordo-pink/backend-util-extract-request-ip" +import { is_finite_non_negative_int } from "@ordo-pink/tau" +import { log_request } from "@ordo-pink/backend-util-log-request" +import { routary_cors } from "@ordo-pink/routary-cors" +import { set_content_type_application_json_header } from "@ordo-pink/backend-util-set-header" + +import { type TDTChamber, type TDTContext } from "./backend-dt.types" + +// TODO Extract colonoscope from Routary +// TODO WebSocket for dt-dt and dt-web notifications +export const create_backend_dt = (chamber: TDTChamber) => + Routary.Of({ ...chamber, headers: new Headers(), request_ip: null, status: 200 }) + .head("/:uid/:fsid", intake => { + const context = { ...intake, status: 204, request_ip: null, headers: intake.headers ?? new Headers() } + + return Oath.Resolve(context) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.chain(validate_request_params)) + .pipe(ops0.chain(authenticate)) + .pipe(ops0.chain(check_authorization(context))) + .pipe(ops0.map(extract_ids(context))) + .pipe(ops0.chain(check_file_exists(context))) + .pipe(ops0.chain(set_last_modified_header(context))) + .pipe(ops0.map(() => context)) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake: context }))) + .fix(status_from_rrr) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.map(create_response)) + .invoke(invokers0.force_resolve) + }) + + .get("/:uid/:fsid", intake => { + const context = { ...intake, status: 200, request_ip: null, headers: intake.headers ?? new Headers() } + + return Oath.Resolve(context) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.chain(validate_request_params)) + .pipe(ops0.chain(authenticate)) + .pipe(ops0.chain(check_authorization(context))) + .pipe(ops0.map(extract_ids(context))) + .pipe(ops0.chain(check_file_exists(context))) + .pipe(ops0.chain(set_last_modified_header(context))) + .pipe(ops0.chain(({ uid, fsid }) => context.data_persistence_strategy.read(uid, fsid))) + .pipe(ops0.tap(file => void (context.payload = file))) + .pipe(ops0.map(() => context)) + .pipe(ops0.tap(context => context.headers.set("Content-Type", "application/octet-stream"))) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake: context }))) + .fix(status_from_rrr) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.map(create_response)) + .invoke(invokers0.force_resolve) + }) + + .post( + "/:uid/:fsid", + default_handler(intake => + validate_request_params(intake) + .pipe(ops0.chain(validate_body_is_not_empty)) + .pipe(ops0.chain(authenticate)) + .pipe(ops0.chain(check_authorization(intake))) + .pipe(ops0.chain(check_can_create_files(intake))) + .pipe(ops0.chain(validate_file_size_limit(intake))) + .pipe(ops0.map(extract_ids(intake))) + .pipe(ops0.chain(check_file_does_not_exist(intake))) + .pipe(ops0.chain(({ uid, fsid }) => intake.data_persistence_strategy.create(uid, fsid, intake.req.body!))) + .pipe(ops0.map(() => intake)) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake }))), + ), + ) + + .put( + "/:uid/:fsid", + default_handler(intake => + validate_request_params(intake) + .pipe(ops0.chain(validate_body_is_not_empty)) + .pipe(ops0.chain(authenticate)) + .pipe(ops0.chain(check_authorization(intake))) + .pipe(ops0.chain(check_total_files_limit_if_file_does_not_exist(intake))) + .pipe(ops0.chain(validate_file_size_limit(intake))) + .pipe(ops0.map(extract_ids(intake))) + .pipe(ops0.chain(({ uid, fsid }) => intake.data_persistence_strategy.update(uid, fsid, intake.req.body!))) + .pipe(ops0.map(() => intake)) + .pipe(ops0.map(() => intake)) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake }))), + ), + ) + + .delete( + "/:uid/:fsid", + default_handler(intake => + validate_request_params(intake) + .pipe(ops0.chain(authenticate)) + .pipe(ops0.chain(check_authorization(intake))) + .pipe(ops0.map(extract_ids(intake))) + .pipe(ops0.chain(check_file_exists(intake))) + .pipe(ops0.chain(({ uid, fsid }) => intake.data_persistence_strategy.delete(uid, fsid))) + .pipe(ops0.map(() => intake)) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake }))), + ), + ) + + .get("/healthcheck", () => new Response("OK")) + + .use(routary_cors({ allow_origin: chamber.allow_origin, allow_headers: ["content-type", "authorization"] })) + + .start(intake => + Oath.Resolve>({ ...intake, headers: new Headers(), status: 404, request_ip: null }) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.tap(set_content_type_application_json_header)) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.tap(intake => void (intake.payload = "resource not found"))) + .pipe(ops0.map(create_json_response)) + .invoke(invokers0.force_resolve), + ) + +export const validate_request_params = (intake: TIntake) => + Oath.Merge([ + Oath.If(Metadata.Validations.is_fsid(intake.params.fsid)).pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid FSID"))), + Oath.If(CurrentUser.Validations.is_id(intake.params.uid)).pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid UID"))), + ]).pipe(ops0.map(() => intake)) + +export const authenticate = (intake: TIntake) => + Oath.FromNullable(intake.req.headers.get("Authorization")) + .pipe(ops0.map(Authorization => ({ Authorization }))) + .pipe(ops0.map(headers => ({ ...headers, Origin: intake.dt_host }))) // TODO Move to env + .pipe(ops0.map(headers => ({ headers, method: "POST" }))) + .pipe(ops0.chain(init => Oath.FromPromise(() => fetch(`${intake.id_host}/tokens/validate`, init)))) // TODO Move to env + .pipe(ops0.chain(res => Oath.FromPromise(() => res.json()))) + .pipe(ops0.chain(body => Oath.If(body?.success, { T: () => body.payload }))) + .pipe(ops0.chain(x => Oath.If(CurrentUser.Validations.is_dto(x), { T: () => x as Ordo.User.Current.DTO }))) + .pipe(ops0.rejected_map(e => RRR.codes.eacces(e?.message ?? "Unauthorized"))) + +// TODO checking permissions for editing files of other users +export const check_authorization = (intake: TIntake) => (user: Ordo.User.Current.DTO) => + Oath.If(user.id === intake.params.uid) + .pipe(ops0.map(() => user)) + .pipe(ops0.rejected_map(() => RRR.codes.eperm("Permission denied"))) + +export const check_file_exists = + (intake: TIntake) => + ({ uid, fsid }: TIDs) => + intake.data_persistence_strategy + .exists(uid, fsid) + .pipe(ops0.chain(exists => Oath.If(exists))) + .pipe(ops0.map(() => ({ uid, fsid }))) + .pipe(ops0.rejected_map(() => RRR.codes.enoent("File not found"))) + +export type TIDs = { uid: Ordo.User.UID; fsid: Ordo.Metadata.FSID } +export const extract_ids = (intake: TIntake) => () => ({ + uid: intake.params.uid as Ordo.User.UID, + fsid: intake.params.fsid as Ordo.Metadata.FSID, +}) + +const check_file_does_not_exist = + (intake: TIntake) => + ({ uid, fsid }: TIDs) => + intake.data_persistence_strategy + .exists(uid, fsid) + .pipe(ops0.chain(exists => Oath.If(!exists))) + .pipe(ops0.map(() => ({ uid, fsid }))) + .pipe(ops0.rejected_map(() => RRR.codes.eexist("File already exists"))) + +export const validate_body_is_not_empty = (intake: TIntake) => + Oath.FromNullable(intake.req.body) + .pipe(ops0.map(() => intake)) + .pipe(ops0.rejected_map(() => RRR.codes.einval("Empty file body"))) + +export const validate_file_size_limit = (intake: TIntake) => (user: Ordo.User.Current.DTO) => + Oath.FromNullable(intake.req.headers.get("content-length")) + .pipe(ops0.map(file_size => Number.parseInt(file_size, 10))) + .pipe(ops0.chain(file_size => Oath.If(is_finite_non_negative_int(file_size), { T: () => file_size }))) + .pipe(ops0.chain(file_size => Oath.If(CurrentUser.FromDTO(user).can_upload(file_size)))) + .pipe(ops0.rejected_map(() => RRR.codes.efbig("File too big"))) + +// TODO check if attemted to create a file in other user's space +export const check_can_create_files = (intake: TIntake) => (user: Ordo.User.Current.DTO) => + intake.data_persistence_strategy + .read(user.id, METADATA_CONTENT_FSID) + .pipe(ops0.map(metadata => metadata.length)) + .fix(() => 0) + .pipe(ops0.map(total_files => CurrentUser.FromDTO(user).can_create_files(total_files))) + .pipe(ops0.chain(can_create => Oath.If(can_create))) + .pipe(ops0.map(() => user)) + .pipe(ops0.rejected_map(() => RRR.codes.enospc("Too many files"))) + +const check_total_files_limit_if_file_does_not_exist = (intake: TIntake) => (user: Ordo.User.Current.DTO) => + intake.data_persistence_strategy + .exists(intake.params.uid as Ordo.User.UID, intake.params.fsid as Ordo.Metadata.FSID) + .pipe(ops0.chain(exists => (exists ? Oath.Resolve(user) : check_can_create_files(intake)(user)))) + +const set_last_modified_header = + (intake: TIntake) => + ({ uid, fsid }: TIDs) => + intake.data_persistence_strategy + .mtime(uid, fsid) + .pipe(ops0.map(mtime => new Date(mtime))) + .pipe(ops0.map(date => date.toUTCString())) + .pipe(ops0.tap(last_modified => intake.headers.set("Last-Modified", last_modified))) + .pipe(ops0.map(() => ({ uid, fsid }))) diff --git a/lib/backend-dt/src/backend-dt.test.ts b/lib/backend-dt/src/backend-dt.test.ts new file mode 100644 index 000000000..361933922 --- /dev/null +++ b/lib/backend-dt/src/backend-dt.test.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { expect, test } from "bun:test" +import { backend_dt } from "./backend-dt.impl" + +test("backend-dt should pass", () => { + expect(backend_dt).toEqual("backend-dt") +}) diff --git a/lib/backend-dt/src/backend-dt.types.ts b/lib/backend-dt/src/backend-dt.types.ts new file mode 100644 index 000000000..79de7247b --- /dev/null +++ b/lib/backend-dt/src/backend-dt.types.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import type { TDefaultContext } from "@ordo-pink/backend-util-default-handler" +import type { TLogger } from "@ordo-pink/logger" + +export type TDTChamber = { + allow_origin: string[] + data_persistence_strategy: OrdoBackend.Data.PersistenceStrategy + logger: TLogger + id_host: string + dt_host: string +} + +export type TDTContext = TDefaultContext & TDTChamber diff --git a/lib/backend-email-strategy-rusender/index.ts b/lib/backend-email-strategy-rusender/index.ts deleted file mode 100644 index 0b731168d..000000000 --- a/lib/backend-email-strategy-rusender/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export * from "./src/backend-email-strategy-rusender.impl" diff --git a/lib/backend-email-strategy-rusender/readme.md b/lib/backend-email-strategy-rusender/readme.md deleted file mode 100644 index c47e458a0..000000000 --- a/lib/backend-email-strategy-rusender/readme.md +++ /dev/null @@ -1,4 +0,0 @@ -# Backend Rusender Email Repository - -This email strategy is intended to be used in RU region and/or CIS countries if there is no local -alternative for sending emails to users. diff --git a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.constants.ts b/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.constants.ts deleted file mode 100644 index 848cc0c2f..000000000 --- a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.constants.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { TEmailStrategy } from "@ordo-pink/backend-service-offline-notifications" - -export const RS_TEMPLATE_URL = "https://api.beta.rusender.ru/api/v1/external-mails/send-by-template" - -export const RS_SEND_URL = "https://api.beta.rusender.ru/api/v1/external-mails/send" - -export const RS_SEND_HTTP_METHOD = "POST" as const - -export const RS_HEADERS = { "Content-Type": "application/json" } - -export const RS_APIKEY_HEADER = "X-Api-Key" - -export const RusenderEmailSubject: Record, string> = { - sendChangeEmailEmail: "Подтверждение изменения адреса электронной почты в Ordo.pink", - sendEmailChangeRequestedEmail: "Получен запрос на изменение вашей электронной почты в Ordo.pink", - sendPasswordChangedEmail: "Пароль от вашей учётной записи в Ordo.pink изменён", - sendRecoverPasswordEmail: "Ваша ссылка для восстановления пароля учётной записи в Ordo.pink", - sendConfirmationEmail: "Ваша ссылка для подтверждения электронной почты от учётной записи в Ordo.pink", - sendSignUpEmail: "Добро пожаловать в Ordo.pink!", - sendSignInEmail: "Кто-то вошёл в ваш аккаунт Ordo.pink", -} - -/** - * TODO: Add missing template ids - */ -export const RusenderTemplateId: Record, number> = { - sendChangeEmailEmail: 17884, - sendEmailChangeRequestedEmail: 17891, - sendPasswordChangedEmail: 10525, - sendRecoverPasswordEmail: 17893, - sendConfirmationEmail: 17900, - sendSignUpEmail: 9463, - sendSignInEmail: 9661, -} diff --git a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.errors.ts b/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.errors.ts deleted file mode 100644 index 9a8ace179..000000000 --- a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.errors.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export const InvalidRequestBodyErrorCode = 400 - -export const InvalidAPIKeyErrorCode = 401 - -export const BalanceDrainedErrorCode = 402 - -export const DomainVerificationFailedErrorCode = 403 - -export const UserDomainApiKeyNotFoundErrorCode = 404 - -export const UserUnsubscribedErrorCode = 422 - -export const ServiceTemporaryUnavailableErrorCode = 503 - -export type EmailStrategyRusenderError = - | typeof InvalidRequestBodyErrorCode - | typeof InvalidAPIKeyErrorCode - | typeof BalanceDrainedErrorCode - | typeof DomainVerificationFailedErrorCode - | typeof UserDomainApiKeyNotFoundErrorCode - | typeof UserUnsubscribedErrorCode - | typeof ServiceTemporaryUnavailableErrorCode diff --git a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.impl.ts b/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.impl.ts deleted file mode 100644 index f700d68f5..000000000 --- a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.impl.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { RusenderEmailSubject, RusenderTemplateId } from "./backend-email-strategy-rusender.constants" -import { type TEmailStrategyRusenderStatic } from "./backend-email-strategy-rusender.types" -import { sendAsync } from "./methods/send-async" -import { sendTemplateAsync } from "./methods/send-template-async" - -/** - * `RusenderEmailStrategy` implements `EmailStrategy` for sending emails using Rusender. To create - * a `RusenderEmailStrategy`, you need to provide Rusender API key. You can get a Rusender API key - * at beta.rusender.ru. - * - * @see https://rusender.ru - * @warning This strategy is recommended for use in "ru" region only. - * - * @example - * const emailStrategy = RusenderEmailStrategy.of({ key: "YOUR_RUSENDER_API_KEY" }) - * emailStrategy.send(message) - * - * TODO: #272 Support for `sendSync` that awaits the result. - * TODO: #273 Support for receiving `sendAsync` result and propagating it to an optional listener. - * TODO: #274 Drop using templates and provide markup directly. - * TODO: #275 Update OfflineNotificationsService types to avoid type collisions. - */ -export const EmailStrategyRusender: TEmailStrategyRusenderStatic = { - create: params => { - const sendTemplate = sendTemplateAsync(params) - - return { - send_async: sendAsync(params), - send_change_email: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - new_email: newEmail, - old_email: oldEmail, - confirmation_url: confirmationUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendChangeEmailEmail, - idTemplateMailUser: RusenderTemplateId.sendChangeEmailEmail, - params: { - telegramChannel, - newEmail, - oldEmail, - supportEmail, - supportTelegram, - confirmationUrl, - }, - }), - send_email_change_requested: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - new_email: newEmail, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendEmailChangeRequestedEmail, - idTemplateMailUser: RusenderTemplateId.sendEmailChangeRequestedEmail, - params: { telegramChannel, newEmail, supportEmail, supportTelegram }, - }), - send_password_changed: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - reset_password_url: resetPasswordUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendPasswordChangedEmail, - idTemplateMailUser: RusenderTemplateId.sendPasswordChangedEmail, - params: { telegramChannel, supportEmail, supportTelegram, resetPasswordUrl }, - }), - send_recover_password: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - password_recovery_url: passwordRecoveryUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendRecoverPasswordEmail, - idTemplateMailUser: RusenderTemplateId.sendRecoverPasswordEmail, - params: { telegramChannel, supportEmail, supportTelegram, passwordRecoveryUrl }, - }), - send_email_confirmation: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - confirmation_url: confirmationUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendConfirmationEmail, - idTemplateMailUser: RusenderTemplateId.sendConfirmationEmail, - params: { telegramChannel, supportEmail, supportTelegram, confirmationUrl }, - }), - send_sign_up: ({ - telegramChannel, - from, - to, - support_email: supportEmail, - support_channels: supportTelegram, - confirmation_url: confirmationUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendSignUpEmail, - idTemplateMailUser: RusenderTemplateId.sendSignUpEmail, - params: { telegramChannel, confirmationUrl, supportEmail, supportTelegram }, - }), - send_sign_in: ({ - telegramChannel, - from, - to, - ip, - support_email: supportEmail, - support_channels: supportTelegram, - reset_password_url: resetPasswordUrl, - }) => - sendTemplate({ - from, - to, - subject: RusenderEmailSubject.sendSignInEmail, - idTemplateMailUser: RusenderTemplateId.sendSignInEmail, - params: { telegramChannel, ip, resetPasswordUrl, supportEmail, supportTelegram }, - }), - } - }, -} diff --git a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.types.ts b/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.types.ts deleted file mode 100644 index 04f370e1f..000000000 --- a/lib/backend-email-strategy-rusender/src/backend-email-strategy-rusender.types.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import type { TEmailParams, TEmailStrategy } from "@ordo-pink/backend-service-offline-notifications" -import type { UUIDv4 } from "@ordo-pink/tau" - -import type { RS_APIKEY_HEADER, RS_HEADERS, RS_SEND_HTTP_METHOD } from "./backend-email-strategy-rusender.constants" -import type { EmailStrategyRusenderError } from "./backend-email-strategy-rusender.errors" - -/** - * Email strategy Rusender params. The only required parameter is the API key. - */ -export type TEmailStrategyRusenderParams = { key: string } - -/** - * EmailStrategyRusender static methods descriptor. - */ -export type TEmailStrategyRusenderStatic = { - create: (params: TEmailStrategyRusenderParams) => TEmailStrategy -} - -/** - * Successful Rusender request response. The default status code is `201`. - */ -export type TRusenderSendEmailResponseBody = { uuid: UUIDv4 } - -/** - * Failed Rusender request response. - */ -export type TRusenderSendEmailErrorResponseBody = { - message: string - statusCode: EmailStrategyRusenderError -} - -export type TRusenderRequestHeaders = typeof RS_HEADERS & { - [RS_APIKEY_HEADER]: string -} - -export type TRusenderSendRusenderRequestParams = { - method: typeof RS_SEND_HTTP_METHOD - url: string - body: string - headers: TRusenderRequestHeaders -} - -export type TEmailStrategyRusenderMethod = ( - params: TEmailStrategyRusenderParams, -) => TEmailStrategy[K] - -export type TRusenderEmailTemplate = Omit & { - idTemplateMailUser?: number - params: Record -} diff --git a/lib/backend-email-strategy-rusender/src/methods/send-async.ts b/lib/backend-email-strategy-rusender/src/methods/send-async.ts deleted file mode 100644 index cf0d17f66..000000000 --- a/lib/backend-email-strategy-rusender/src/methods/send-async.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { chain_oath, from_nullable_oath, from_promise_oath, map_oath, of_oath, or_nothing_oath } from "@ordo-pink/oath" -import { type TEmailParams } from "@ordo-pink/backend-service-offline-notifications" -import { extend } from "@ordo-pink/tau" - -import { RS_APIKEY_HEADER, RS_HEADERS, RS_SEND_HTTP_METHOD, RS_SEND_URL } from "../backend-email-strategy-rusender.constants" -import { - type TEmailStrategyRusenderMethod, - type TRusenderSendRusenderRequestParams, -} from "../backend-email-strategy-rusender.types" - -export const sendAsync: TEmailStrategyRusenderMethod<"sendAsync"> = - ({ key }) => - message => - void of_oath(message) - .pipe(chain_oath(message => createDefaultRequest0(message, key))) - .pipe(chain_oath(fetch0)) - .invoke(or_nothing_oath) - -// --- Internal --- - -const initRequestParams = (key: string) => ({ headers: { ...RS_HEADERS, [RS_APIKEY_HEADER]: key } }) -const addMethod = () => ({ method: RS_SEND_HTTP_METHOD }) -const addUrl = (url: string) => () => ({ url }) -const addBody = (mail: any) => () => ({ body: JSON.stringify({ mail }) }) - -const fetch0 = ({ method, url, headers, body }: TRusenderSendRusenderRequestParams) => - from_promise_oath(() => fetch(url, { method, headers, body })) - -const createCommonRequest0 = (key: string, mail: TEmailParams) => - from_nullable_oath(key) - .pipe(map_oath(initRequestParams)) - .pipe(map_oath(extend(addMethod))) - .pipe(map_oath(extend(addBody(mail)))) - -const createDefaultRequest0 = (mail: TEmailParams, key: string) => - createCommonRequest0(key, mail).pipe(map_oath(extend(addUrl(RS_SEND_URL)))) diff --git a/lib/backend-email-strategy-rusender/src/methods/send-template-async.ts b/lib/backend-email-strategy-rusender/src/methods/send-template-async.ts deleted file mode 100644 index 062fb0e96..000000000 --- a/lib/backend-email-strategy-rusender/src/methods/send-template-async.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { chain_oath, from_nullable_oath, from_promise_oath, map_oath, of_oath, or_nothing_oath } from "@ordo-pink/oath" -import { type TEmailParams } from "@ordo-pink/backend-service-offline-notifications" -import { extend } from "@ordo-pink/tau" - -import { - RS_APIKEY_HEADER, - RS_HEADERS, - RS_SEND_HTTP_METHOD, - RS_TEMPLATE_URL, -} from "../backend-email-strategy-rusender.constants" -import { - TEmailStrategyRusenderParams, - type TRusenderEmailTemplate, - type TRusenderSendRusenderRequestParams, -} from "../backend-email-strategy-rusender.types" - -export const sendTemplateAsync = - ({ key }: TEmailStrategyRusenderParams) => - (template: TRusenderEmailTemplate) => - void of_oath(template) - .pipe(chain_oath(template => createDefaultRequest0(template, key))) - .pipe(chain_oath(fetch0)) - .invoke(or_nothing_oath) - -// --- Internal --- - -const initRequestParams = (key: string) => ({ headers: { ...RS_HEADERS, [RS_APIKEY_HEADER]: key } }) -const addMethod = () => ({ method: RS_SEND_HTTP_METHOD }) -const addUrl = (url: string) => () => ({ url }) -const addBody = (mail: any) => () => ({ body: JSON.stringify({ mail }) }) - -const fetch0 = ({ method, url, headers, body }: TRusenderSendRusenderRequestParams) => - from_promise_oath(() => fetch(url, { method, headers, body })) - -const createCommonRequest0 = (key: string, mail: TEmailParams) => - from_nullable_oath(key) - .pipe(map_oath(initRequestParams)) - .pipe(map_oath(extend(addMethod))) - .pipe(map_oath(extend(addBody(mail)))) - -const createDefaultRequest0 = (mail: TEmailParams, key: string) => - createCommonRequest0(key, mail).pipe(map_oath(extend(addUrl(RS_TEMPLATE_URL)))) diff --git a/lib/backend-id/src/backend-id.impl.ts b/lib/backend-id/src/backend-id.impl.ts index 3a2259694..ec6d59bbd 100644 --- a/lib/backend-id/src/backend-id.impl.ts +++ b/lib/backend-id/src/backend-id.impl.ts @@ -22,13 +22,13 @@ import { Oath, invokers0, ops0 } from "@ordo-pink/oath" import { Routary, type TIntake } from "@ordo-pink/routary" import { set_x_response_time_header, start_response_timer, stop_response_timer } from "@ordo-pink/backend-util-response-time" -import { create_response } from "@ordo-pink/backend-util-create-response" +import { create_json_response } from "@ordo-pink/backend-util-create-response" import { extract_request_ip } from "@ordo-pink/backend-util-extract-request-ip" import { log_request } from "@ordo-pink/backend-util-log-request" import { routary_cors } from "@ordo-pink/routary-cors" import { set_content_type_application_json_header } from "@ordo-pink/backend-util-set-header" -import { type TIDChamber, type TSharedContext } from "./backend-id.types" +import { type TIDChamber, type TIDContext } from "./backend-id.types" import { handle_delete_user } from "./handlers/user/delete-user.handler" import { handle_get_user_by_handle } from "./handlers/user/get-user-by-handle.handler" import { handle_get_user_by_id } from "./handlers/user/get-user-by-id.handler" @@ -41,7 +41,7 @@ import { handle_validate_token } from "./handlers/tokens/validate.handler" // TODO Global stats when API is ready export const create_backend_id = (chamber: TIDChamber) => - Routary.Of(chamber) + Routary.Of({ ...chamber, request_ip: null, status: 200, headers: new Headers() }) .post("/codes/request", handle_request_code) .post("/codes/validate", handle_validate_code) @@ -56,11 +56,11 @@ export const create_backend_id = (chamber: TIDChamber) => .get("/healthcheck", () => new Response("OK")) // TODO Extract to lib - .use(routary_cors({ allow_origin: chamber.allow_origin, allow_headers: ["Content-Type"] })) + .use(routary_cors({ allow_origin: chamber.allow_origin, allow_headers: ["content-type", "authorization"] })) .start(intake => // TODO Extract to lib - Oath.Resolve>({ ...intake, headers: {}, status: 404, request_ip: null }) + Oath.Resolve>({ ...intake, headers: new Headers(), status: 404, request_ip: null }) .pipe(ops0.tap(start_response_timer)) .pipe(ops0.tap(extract_request_ip)) .pipe(ops0.tap(set_content_type_application_json_header)) @@ -68,6 +68,6 @@ export const create_backend_id = (chamber: TIDChamber) => .pipe(ops0.tap(set_x_response_time_header)) .pipe(ops0.tap(log_request)) .pipe(ops0.tap(intake => void (intake.payload = "resource not found"))) - .pipe(ops0.map(create_response)) + .pipe(ops0.map(create_json_response)) .invoke(invokers0.force_resolve), ) diff --git a/lib/backend-id/src/backend-id.types.ts b/lib/backend-id/src/backend-id.types.ts index e9a34395e..aa7a8403d 100644 --- a/lib/backend-id/src/backend-id.types.ts +++ b/lib/backend-id/src/backend-id.types.ts @@ -19,34 +19,20 @@ * along with this program. If not, see . */ -import { SocketAddress } from "bun" - +import type { TDefaultContext } from "@ordo-pink/backend-util-default-handler" import type { TLogger } from "@ordo-pink/logger" -import { TWJWT } from "@ordo-pink/wjwt" - -// TODO Move to lib +import type { TWJWT } from "@ordo-pink/wjwt" export type TIDChamber = { allow_origin: string[] + defaults: { file_limit: number; max_upload_size: number; max_functions: number } logger: TLogger - user_persistence_strategy: OrdoBackend.User.PersistenceStrategy - token_persistence_strategy: OrdoBackend.Token.PersistenceStrategy notification_strategy: OrdoBackend.Notification.EmailStrategy - wjwt: TWJWT persisted_token_lifetime: number - status: number - headers: Record - defaults: { - file_limit: number - max_upload_size: number - max_functions: number - } + token_persistence_strategy: OrdoBackend.Token.PersistenceStrategy + user_persistence_strategy: OrdoBackend.User.PersistenceStrategy + wjwt: TWJWT + web_host: string } -export type TSharedContext<$TPayload = unknown> = TIDChamber & { - response_timer?: [number, number] - response_time?: string - payload?: $TPayload - - request_ip: SocketAddress | null -} +export type TIDContext = TDefaultContext & TIDChamber diff --git a/lib/backend-id/src/common/check-if-edited-user-is-current-user.ts b/lib/backend-id/src/common/check-if-edited-user-is-current-user.ts index 55fa26156..ea45417b5 100644 --- a/lib/backend-id/src/common/check-if-edited-user-is-current-user.ts +++ b/lib/backend-id/src/common/check-if-edited-user-is-current-user.ts @@ -1,13 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath } from "@ordo-pink/oath" import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" import { get_token_from_authorization_header } from "./get-auth-token-from-authorization-header" import { get_user_from_token } from "./get-user-from-token" import { verify_auth_token } from "./verify-auth-token" -export const check_if_edited_user_is_current_user = (i: TIntake) => +export const check_if_edited_user_is_current_user = (i: TIntake) => get_token_from_authorization_header(i) .and(verify_auth_token(i)) .and(get_user_from_token(i)) diff --git a/lib/backend-id/src/common/create-auth-token.ts b/lib/backend-id/src/common/create-auth-token.ts index 63f999e2c..4cd75807d 100644 --- a/lib/backend-id/src/common/create-auth-token.ts +++ b/lib/backend-id/src/common/create-auth-token.ts @@ -1,17 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { type TIntake } from "@ordo-pink/routary" import { unknown_error } from "@ordo-pink/backend-util-extract-body" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const create_auth_token = (intake: TIntake) => (user: OrdoInternal.User.PrivateDTO) => +export const create_auth_token = (intake: TIntake) => (user: OrdoBackend.User.DTO) => Oath.FromPromise(() => - intake.wjwt.sign({ - sub: user.id, - lim: user.file_limit, - mus: user.max_upload_size, - sbs: user.subscription, - }), + intake.wjwt.sign({ sub: user.id, lim: user.file_limit, mus: user.max_upload_size, sbs: user.subscription }), ) .pipe(ops0.map(jwt => ({ jwt, user }))) .pipe(ops0.rejected_map(rrr => unknown_error(rrr, intake))) diff --git a/lib/backend-id/src/common/extract-body-email.ts b/lib/backend-id/src/common/extract-body-email.ts index 623f02626..04ba1ec8b 100644 --- a/lib/backend-id/src/common/extract-body-email.ts +++ b/lib/backend-id/src/common/extract-body-email.ts @@ -1,11 +1,32 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { CurrentUser } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" import { email_missing_rrr, invalid_email_rrr } from "../rrrs/invalid-user-email.rrr" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const extract_body_email = (intake: TIntake) => (request_body: any) => +export const extract_body_email = (intake: TIntake) => (request_body: any) => Oath.FromNullable(request_body.email) .pipe(ops0.rejected_map(() => email_missing_rrr(intake))) .pipe(ops0.chain(email => Oath.If(is_email(email), { T: () => email, F: () => invalid_email_rrr(email, intake) }))) diff --git a/lib/backend-id/src/common/get-auth-token-from-authorization-header.ts b/lib/backend-id/src/common/get-auth-token-from-authorization-header.ts index d3ebb3d41..2004f70f8 100644 --- a/lib/backend-id/src/common/get-auth-token-from-authorization-header.ts +++ b/lib/backend-id/src/common/get-auth-token-from-authorization-header.ts @@ -1,10 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const get_token_from_authorization_header = (intake: TIntake) => +export const get_token_from_authorization_header = (intake: TIntake) => Oath.FromNullable(intake.req.headers.get("authorization")) .pipe(ops0.rejected_map(() => ({ rrr: RRR.codes.eacces("Missing Authorization header"), intake }))) .pipe(ops0.map(header => header.replace("Bearer ", ""))) diff --git a/lib/backend-id/src/common/get-user-from-token.ts b/lib/backend-id/src/common/get-user-from-token.ts index 26a121dbd..a8d2ed32a 100644 --- a/lib/backend-id/src/common/get-user-from-token.ts +++ b/lib/backend-id/src/common/get-user-from-token.ts @@ -1,10 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const get_user_from_token = (intake: TIntake) => (token: string) => +export const get_user_from_token = (intake: TIntake) => (token: string) => Oath.Try(() => intake.wjwt.decode(token)) .pipe(ops0.rejected_map(error => RRR.codes.einval(error.message, error.name, error.cause, error.stack))) .and(token => token.payload.sub) diff --git a/lib/backend-id/src/common/persist-token.ts b/lib/backend-id/src/common/persist-token.ts index 36f75f2fe..88272fb70 100644 --- a/lib/backend-id/src/common/persist-token.ts +++ b/lib/backend-id/src/common/persist-token.ts @@ -1,13 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { type TIntake } from "@ordo-pink/routary" import { type TWJWTSignResult } from "@ordo-pink/wjwt" import { ops0 } from "@ordo-pink/oath" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const persist_token = - (intake: TIntake) => - ({ jwt, user }: { jwt: TWJWTSignResult; user: OrdoInternal.User.PrivateDTO }) => - intake.token_persistence_strategy - .set_token(jwt.payload.sub, jwt.payload.jti, { exp: jwt.payload.exp + intake.persisted_token_lifetime }) - .and(() => ({ jwt, user })) - .pipe(ops0.rejected_map(rrr => ({ rrr, intake }))) +export const persist_token = (intake: TIntake) => (params: { jwt: TWJWTSignResult; user: OrdoBackend.User.DTO }) => + intake.token_persistence_strategy + .set_token(params.jwt.payload.sub, params.jwt.payload.jti, { + exp: params.jwt.payload.exp + intake.persisted_token_lifetime, + }) + .and(() => params) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake }))) diff --git a/lib/backend-id/src/common/remove-token.ts b/lib/backend-id/src/common/remove-token.ts index c343f9a72..fbc3268e4 100644 --- a/lib/backend-id/src/common/remove-token.ts +++ b/lib/backend-id/src/common/remove-token.ts @@ -1,10 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { type TIntake } from "@ordo-pink/routary" import { unknown_error } from "@ordo-pink/backend-util-extract-body" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const remove_token = (intake: TIntake) => (token: string) => +export const remove_token = (intake: TIntake) => (token: string) => Oath.Try(() => intake.wjwt.decode(token)) .pipe(ops0.rejected_map(error => unknown_error(error, intake))) .and(jwt => jwt.payload) diff --git a/lib/backend-id/src/common/validate-id-param.ts b/lib/backend-id/src/common/validate-id-param.ts index 8c6a3fd85..72474c118 100644 --- a/lib/backend-id/src/common/validate-id-param.ts +++ b/lib/backend-id/src/common/validate-id-param.ts @@ -1,11 +1,32 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { CurrentUser } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" import { invalid_id_rrr } from "../rrrs/invalid-user-id.rrr" -export const check_if_id_param_is_valid = (intake: TIntake) => +export const check_if_id_param_is_valid = (intake: TIntake) => Oath.Resolve(intake.params.user_id) .and(id => Oath.If(is_id(id))) .pipe(ops0.rejected_map(() => invalid_id_rrr(intake.params.user_id, intake))) diff --git a/lib/backend-id/src/common/verify-auth-token.ts b/lib/backend-id/src/common/verify-auth-token.ts index 884872331..274d87023 100644 --- a/lib/backend-id/src/common/verify-auth-token.ts +++ b/lib/backend-id/src/common/verify-auth-token.ts @@ -1,26 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" import { unknown_error } from "@ordo-pink/backend-util-extract-body" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" import { invalid_token_rrr } from "../rrrs/invalid-token.rrr" -export const verify_auth_token = (intake: TIntake) => (token: string) => +export const verify_auth_token = (intake: TIntake) => (token: string) => Oath.FromPromise(() => intake.wjwt.verify(token)) .pipe(ops0.rejected_map(error => ({ rrr: RRR.codes.einval(error.message, error.name, error.cause, error.stack), intake }))) .and(v => Oath.If(v, { T: () => token, F: () => invalid_token_rrr(intake) })) .and(token => Oath.Try(() => intake.wjwt.decode(token)) .pipe(ops0.rejected_map(error => unknown_error(error, intake))) - .and(({ payload }) => intake.token_persistence_strategy.get_token(payload.sub, payload.jti)) + .and(({ payload }) => + intake.token_persistence_strategy + .get_token(payload.sub, payload.jti) + .pipe(ops0.rejected_map(() => RRR.codes.enoent("Token not found"))), + ) .and(({ exp }) => Oath.If(exp * 1000 >= Date.now(), { F: () => invalid_token_rrr(intake) })) .and(() => token), ) -export const verify_persisted_auth_token = (intake: TIntake) => (token: string) => +export const verify_persisted_auth_token = (intake: TIntake) => (token: string) => Oath.Try(() => intake.wjwt.decode(token)) .pipe(ops0.rejected_map(error => unknown_error(error, intake))) - .and(({ payload }) => intake.token_persistence_strategy.get_token(payload.sub, payload.jti)) + .and(({ payload }) => + intake.token_persistence_strategy.get_token(payload.sub, payload.jti).pipe(ops0.rejected_map(rrr => ({ rrr, intake }))), + ) .and(({ exp }) => Oath.If(exp * 1000 >= Date.now(), { F: () => invalid_token_rrr(intake) })) .and(() => token) diff --git a/lib/backend-id/src/handlers/codes/request-code.handler.ts b/lib/backend-id/src/handlers/codes/request-code.handler.ts index 8e3d46f41..e7d783965 100644 --- a/lib/backend-id/src/handlers/codes/request-code.handler.ts +++ b/lib/backend-id/src/handlers/codes/request-code.handler.ts @@ -1,13 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { extract_request_body, unknown_error } from "@ordo-pink/backend-util-extract-body" import { type TIntake } from "@ordo-pink/routary" import { UserSubscription } from "@ordo-pink/core" +import { default_handler } from "@ordo-pink/backend-util-default-handler" -import { type TSharedContext } from "../../backend-id.types" -import { default_handler } from "../default.handler" +import { type TIDContext } from "../../backend-id.types" import { extract_body_email } from "../../common/extract-body-email" -export const handle_request_code = default_handler(intake => +export const handle_request_code = default_handler(intake => extract_request_body(intake) .pipe(ops0.chain(extract_body_email(intake))) .pipe(ops0.chain(get_or_create_user(intake))) @@ -18,14 +39,14 @@ export const handle_request_code = default_handler(intake => // --- Internal --- -type I = TIntake +type I = TIntake const hash_argon2 = (intake: I) => (code: string) => Oath.FromPromise(() => Bun.password.hash(code)) .pipe(ops0.map(hash => ({ code, hash }))) .pipe(ops0.rejected_map(e => unknown_error(e, intake))) -const create_handle = (email: Ordo.User.Email, id: Ordo.User.ID) => +const create_handle = (email: Ordo.User.Email, id: Ordo.User.UID) => `@${email.split("@")[0].replaceAll(".", "_").replaceAll("/", "")}${id.split("-")[0]}` as Ordo.User.Handle const check_handle_is_free = (handle: Ordo.User.Handle, intake: I) => @@ -70,7 +91,7 @@ const update_user_code = (i: I) => (user: OrdoBackend.User.DTO) => ), ) -const create_user = (email: Ordo.User.Email, id: Ordo.User.ID, handle: Ordo.User.Handle) => (i: I) => +const create_user = (email: Ordo.User.Email, id: Ordo.User.UID, handle: Ordo.User.Handle) => (i: I) => i.user_persistence_strategy .create({ created_at: Date.now(), @@ -87,11 +108,11 @@ const create_user = (email: Ordo.User.Email, id: Ordo.User.ID, handle: Ordo.User type P2 = { codes: { code: string; hash: string }; email: Ordo.User.Email } const send_code = - (i: I) => + (intake: I) => ({ codes, email }: P2) => // TODO Create email with Maoka - i.notification_strategy.send({ + intake.notification_strategy.send({ to: email, subject: "Sign in code for your ORDO account", - content: `Code: ${codes.code}; Link: http://localhost:3004/auth/verify?email=${email}&code=${codes.code}`, + content: `Code: ${codes.code} -> Link: ${intake.web_host}/auth/verify?email=${email}&code=${codes.code}`, }) diff --git a/lib/backend-id/src/handlers/codes/validate-code.handler.ts b/lib/backend-id/src/handlers/codes/validate-code.handler.ts index 19749dbb5..67fe15395 100644 --- a/lib/backend-id/src/handlers/codes/validate-code.handler.ts +++ b/lib/backend-id/src/handlers/codes/validate-code.handler.ts @@ -1,17 +1,38 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { CurrentUser, RRR } from "@ordo-pink/core" import { Oath, ops0 } from "@ordo-pink/oath" import { type TIntake } from "@ordo-pink/routary" +import { default_handler } from "@ordo-pink/backend-util-default-handler" import { extract_request_body } from "@ordo-pink/backend-util-extract-body" import { is_non_empty_string } from "@ordo-pink/tau" -import { type TSharedContext } from "../../backend-id.types" +import { type TIDContext } from "../../backend-id.types" import { create_auth_token } from "../../common/create-auth-token" -import { default_handler } from "../default.handler" import { extract_body_email } from "../../common/extract-body-email" import { persist_token } from "../../common/persist-token" import { redundant_auth_rrr } from "../../rrrs/redundant-auth.rrr" -export const handle_validate_code = default_handler(intake => +export const handle_validate_code = default_handler(intake => extract_request_body(intake) .and(validate_request_body(intake)) .and(validate_user_code(intake)) @@ -25,7 +46,7 @@ export const handle_validate_code = default_handler(intake => // --- Internal --- -type I = TIntake +type I = TIntake const validate_request_body = (intake: I) => (body: any) => Oath.Merge({ diff --git a/lib/backend-id/src/handlers/tokens/invalidate.handler.ts b/lib/backend-id/src/handlers/tokens/invalidate.handler.ts index 642f21eaa..9447c5178 100644 --- a/lib/backend-id/src/handlers/tokens/invalidate.handler.ts +++ b/lib/backend-id/src/handlers/tokens/invalidate.handler.ts @@ -1,10 +1,33 @@ -import { default_handler } from "../default.handler" +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { default_handler } from "@ordo-pink/backend-util-default-handler" + +import { type TIDContext } from "../../backend-id.types" import { get_token_from_authorization_header } from "../../common/get-auth-token-from-authorization-header" import { remove_token } from "../../common/remove-token" import { verify_auth_token } from "../../common/verify-auth-token" // TODO -export const handle_invalidate = default_handler(intake => +export const handle_invalidate = default_handler(intake => get_token_from_authorization_header(intake) .and(verify_auth_token(intake)) .and(remove_token(intake)) diff --git a/lib/backend-id/src/handlers/tokens/refresh.handler.ts b/lib/backend-id/src/handlers/tokens/refresh.handler.ts index 2e280bb20..94c2b531d 100644 --- a/lib/backend-id/src/handlers/tokens/refresh.handler.ts +++ b/lib/backend-id/src/handlers/tokens/refresh.handler.ts @@ -1,12 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { default_handler } from "@ordo-pink/backend-util-default-handler" + +import { type TIDContext } from "../../backend-id.types" import { create_auth_token } from "../../common/create-auth-token" -import { default_handler } from "../default.handler" import { get_token_from_authorization_header } from "../../common/get-auth-token-from-authorization-header" import { get_user_from_token } from "../../common/get-user-from-token" import { persist_token } from "../../common/persist-token" import { remove_token } from "../../common/remove-token" import { verify_persisted_auth_token } from "../../common/verify-auth-token" -export const handle_refresh = default_handler(intake => +export const handle_refresh = default_handler(intake => get_token_from_authorization_header(intake) .and(verify_persisted_auth_token(intake)) .and(remove_token(intake)) diff --git a/lib/backend-id/src/handlers/tokens/validate.handler.ts b/lib/backend-id/src/handlers/tokens/validate.handler.ts index 3f5355a77..84d365215 100644 --- a/lib/backend-id/src/handlers/tokens/validate.handler.ts +++ b/lib/backend-id/src/handlers/tokens/validate.handler.ts @@ -1,11 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { CurrentUser } from "@ordo-pink/core" +import { default_handler } from "@ordo-pink/backend-util-default-handler" -import { default_handler } from "../default.handler" +import { type TIDContext } from "../../backend-id.types" import { get_token_from_authorization_header } from "../../common/get-auth-token-from-authorization-header" import { get_user_from_token } from "../../common/get-user-from-token" import { verify_auth_token } from "../../common/verify-auth-token" -export const handle_validate_token = default_handler(intake => +export const handle_validate_token = default_handler(intake => get_token_from_authorization_header(intake) .and(verify_auth_token(intake)) .and(get_user_from_token(intake)) diff --git a/lib/backend-id/src/handlers/user/delete-user.handler.ts b/lib/backend-id/src/handlers/user/delete-user.handler.ts index 0bead5a76..8126df37d 100644 --- a/lib/backend-id/src/handlers/user/delete-user.handler.ts +++ b/lib/backend-id/src/handlers/user/delete-user.handler.ts @@ -1,12 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" +import { default_handler } from "@ordo-pink/backend-util-default-handler" +import { type TIDContext } from "../../backend-id.types" import { check_if_edited_user_is_current_user } from "../../common/check-if-edited-user-is-current-user" import { check_if_id_param_is_valid } from "../../common/validate-id-param" -import { default_handler } from "../default.handler" -export const handle_delete_user = default_handler(intake => +export const handle_delete_user = default_handler(intake => Oath.Merge([check_if_edited_user_is_current_user(intake), check_if_id_param_is_valid(intake)]) - .and(() => intake.params.user_id as Ordo.User.ID) + .and(() => intake.params.user_id as Ordo.User.UID) .and(id => intake.user_persistence_strategy.remove(id).pipe(ops0.rejected_map(rrr => ({ rrr, intake })))) .and(() => intake), ) diff --git a/lib/backend-id/src/handlers/user/get-user-by-handle.handler.ts b/lib/backend-id/src/handlers/user/get-user-by-handle.handler.ts index 960e17b8e..a3de0f916 100644 --- a/lib/backend-id/src/handlers/user/get-user-by-handle.handler.ts +++ b/lib/backend-id/src/handlers/user/get-user-by-handle.handler.ts @@ -1,12 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { PublicUser } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" +import { default_handler } from "@ordo-pink/backend-util-default-handler" -import { type TSharedContext } from "../../backend-id.types" -import { default_handler } from "../default.handler" +import { type TIDContext } from "../../backend-id.types" import { invalid_handle_rrr } from "../../rrrs/invalid-user-handle.rrr" -export const handle_get_user_by_handle = default_handler(intake => +export const handle_get_user_by_handle = default_handler(intake => Oath.Resolve(intake.params.user_handle) .pipe(ops0.chain(validate_user_handle(intake))) .pipe(ops0.chain(get_user_by_handle(intake))) @@ -17,7 +38,7 @@ export const handle_get_user_by_handle = default_handler(intake => // --- Internal --- -type I = TIntake +type I = TIntake const is_handle = PublicUser.Validations.is_handle diff --git a/lib/backend-id/src/handlers/user/get-user-by-id.handler.ts b/lib/backend-id/src/handlers/user/get-user-by-id.handler.ts index 0bb488523..c72ea6b4d 100644 --- a/lib/backend-id/src/handlers/user/get-user-by-id.handler.ts +++ b/lib/backend-id/src/handlers/user/get-user-by-id.handler.ts @@ -1,12 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { PublicUser } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" +import { default_handler } from "@ordo-pink/backend-util-default-handler" -import { type TSharedContext } from "../../backend-id.types" -import { default_handler } from "../default.handler" +import { type TIDContext } from "../../backend-id.types" import { invalid_id_rrr } from "../../rrrs/invalid-user-id.rrr" -export const handle_get_user_by_id = default_handler(intake => +export const handle_get_user_by_id = default_handler(intake => Oath.Resolve(intake.params.user_id) .pipe(ops0.chain(validate_user_id(intake))) .pipe(ops0.chain(get_by_id(intake))) @@ -17,12 +38,12 @@ export const handle_get_user_by_id = default_handler(intake => // --- Internal --- -type I = TIntake +type I = TIntake const serialize_to_public_user = PublicUser.Serialize const validate_user_id = (intake: I) => (id: unknown) => - Oath.If(PublicUser.Validations.is_id(id), { T: () => id as Ordo.User.ID, F: () => invalid_id_rrr(id, intake) }) + Oath.If(PublicUser.Validations.is_id(id), { T: () => id as Ordo.User.UID, F: () => invalid_id_rrr(id, intake) }) -const get_by_id = (intake: I) => (id: Ordo.User.ID) => +const get_by_id = (intake: I) => (id: Ordo.User.UID) => intake.user_persistence_strategy.get_by_id(id).pipe(ops0.rejected_map(rrr => ({ rrr, intake }))) diff --git a/lib/backend-id/src/handlers/user/update-user.handler.ts b/lib/backend-id/src/handlers/user/update-user.handler.ts index cd1360231..e783e6a1d 100644 --- a/lib/backend-id/src/handlers/user/update-user.handler.ts +++ b/lib/backend-id/src/handlers/user/update-user.handler.ts @@ -1,23 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { Oath, ops0 } from "@ordo-pink/oath" import { CurrentUser } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" +import { default_handler } from "@ordo-pink/backend-util-default-handler" import { extract_request_body } from "@ordo-pink/backend-util-extract-body" import { exists_by_email_rrr, invalid_email_rrr } from "../../rrrs/invalid-user-email.rrr" import { exists_by_handle, invalid_handle_rrr } from "../../rrrs/invalid-user-handle.rrr" import { invalid_first_name_rrr, invalid_installed_functions_rrr, invalid_last_name_rrr } from "../../rrrs/user-field.rrr" -import { type TSharedContext } from "../../backend-id.types" +import { type TIDContext } from "../../backend-id.types" import { check_if_edited_user_is_current_user } from "../../common/check-if-edited-user-is-current-user" import { check_if_id_param_is_valid } from "../../common/validate-id-param" -import { default_handler } from "../default.handler" -export const handle_update_user = default_handler(intake => +export const handle_update_user = default_handler(intake => Oath.Merge([check_if_edited_user_is_current_user(intake), check_if_id_param_is_valid(intake)]) .pipe(ops0.chain(() => extract_request_body(intake))) .pipe(ops0.chain(valdiate_body(intake))) .pipe(ops0.chain(get_current_user(intake))) .pipe(ops0.map(merge_users)) - .pipe(ops0.chain(update_user(intake.params.user_id as Ordo.User.ID, intake))) + .pipe(ops0.chain(update_user(intake.params.user_id as Ordo.User.UID, intake))) .pipe(ops0.map(() => intake)), ) @@ -25,7 +46,7 @@ export const handle_update_user = default_handler(intake => const { is_email, is_handle, is_installed_functions, is_first_name, is_last_name } = CurrentUser.Validations -type I = TIntake +type I = TIntake const check_email_is_not_taken_if_present = (body: Record, i: I) => body.email @@ -69,21 +90,21 @@ const valdiate_body = (i: I) => (body: Record) => check_installed_functions_is_valid_if_present(body, i), check_first_name_is_valid_if_present(body, i), check_last_name_is_valid_if_present(body, i), - ]).pipe(ops0.map(() => body as Partial)) + ]).pipe(ops0.map(() => body as Partial)) -const get_current_user = (i: I) => (updated_user: Partial) => +const get_current_user = (i: I) => (updated_user: Partial) => i.user_persistence_strategy - .get_by_id(i.params.user_id as Ordo.User.ID) + .get_by_id(i.params.user_id as Ordo.User.UID) .pipe(ops0.rejected_map(rrr => ({ rrr, intake: i }))) .pipe(ops0.map(user => ({ user, updated_user }))) -const update_user = (id: Ordo.User.ID, intake: I) => (user: OrdoInternal.User.PrivateDTO) => +const update_user = (id: Ordo.User.UID, intake: I) => (user: OrdoBackend.User.DTO) => intake.user_persistence_strategy.update(id, user).pipe(ops0.rejected_map(rrr => ({ rrr, intake }))) const merge_users = (users: { - user: OrdoInternal.User.PrivateDTO - updated_user: Partial -}): OrdoInternal.User.PrivateDTO => ({ + user: OrdoBackend.User.DTO + updated_user: Partial +}): OrdoBackend.User.DTO => ({ created_at: users.user.created_at, email_code: users.user.email_code, file_limit: users.user.file_limit, diff --git a/lib/backend-id/src/rrrs/invalid-token.rrr.ts b/lib/backend-id/src/rrrs/invalid-token.rrr.ts index e70f773f2..0088e9f78 100644 --- a/lib/backend-id/src/rrrs/invalid-token.rrr.ts +++ b/lib/backend-id/src/rrrs/invalid-token.rrr.ts @@ -1,9 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const invalid_token_rrr = (intake: TIntake) => ({ +export const invalid_token_rrr = (intake: TIntake) => ({ rrr: RRR.codes.eacces("Provided token is invalid"), intake, }) diff --git a/lib/backend-id/src/rrrs/invalid-user-email.rrr.ts b/lib/backend-id/src/rrrs/invalid-user-email.rrr.ts index bbfac85fc..3302b08b5 100644 --- a/lib/backend-id/src/rrrs/invalid-user-email.rrr.ts +++ b/lib/backend-id/src/rrrs/invalid-user-email.rrr.ts @@ -1,19 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const invalid_email_rrr = (email: unknown, intake: TIntake) => ({ +export const invalid_email_rrr = (email: unknown, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid email", email), intake, }) -export const email_missing_rrr = (intake: TIntake) => ({ +export const email_missing_rrr = (intake: TIntake) => ({ rrr: RRR.codes.einval("email not provided"), intake, }) -export const exists_by_email_rrr = (email: string, intake: TIntake) => ({ +export const exists_by_email_rrr = (email: string, intake: TIntake) => ({ rrr: RRR.codes.eexist("user already exists", email), intake, }) diff --git a/lib/backend-id/src/rrrs/invalid-user-handle.rrr.ts b/lib/backend-id/src/rrrs/invalid-user-handle.rrr.ts index 25d16ab0c..912b05839 100644 --- a/lib/backend-id/src/rrrs/invalid-user-handle.rrr.ts +++ b/lib/backend-id/src/rrrs/invalid-user-handle.rrr.ts @@ -1,14 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { TIntake } from "@ordo-pink/routary" -import { TSharedContext } from "../backend-id.types" +import { TIDContext } from "../backend-id.types" -export const invalid_handle_rrr = (handle: string, intake: TIntake) => ({ +export const invalid_handle_rrr = (handle: string, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid user handle", handle), intake, }) -export const exists_by_handle = (handle: string, intake: TIntake) => ({ +export const exists_by_handle = (handle: string, intake: TIntake) => ({ rrr: RRR.codes.eexist("user already exists", handle), intake, }) diff --git a/lib/backend-id/src/rrrs/invalid-user-id.rrr.ts b/lib/backend-id/src/rrrs/invalid-user-id.rrr.ts index e913fa0f1..abe0eb1b4 100644 --- a/lib/backend-id/src/rrrs/invalid-user-id.rrr.ts +++ b/lib/backend-id/src/rrrs/invalid-user-id.rrr.ts @@ -1,9 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const invalid_id_rrr = (id: unknown, intake: TIntake) => ({ +export const invalid_id_rrr = (id: unknown, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid user id", id), intake, }) diff --git a/lib/backend-id/src/rrrs/redundant-auth.rrr.ts b/lib/backend-id/src/rrrs/redundant-auth.rrr.ts index 97d853580..4bbe5d17d 100644 --- a/lib/backend-id/src/rrrs/redundant-auth.rrr.ts +++ b/lib/backend-id/src/rrrs/redundant-auth.rrr.ts @@ -1,9 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const redundant_auth_rrr = (email: Ordo.User.Email, intake: TIntake) => ({ +export const redundant_auth_rrr = (email: Ordo.User.Email, intake: TIntake) => ({ rrr: RRR.codes.eperm("Authentication was not requested", email), intake, }) diff --git a/lib/backend-id/src/rrrs/user-field.rrr.ts b/lib/backend-id/src/rrrs/user-field.rrr.ts index 170ddfa47..c46a52312 100644 --- a/lib/backend-id/src/rrrs/user-field.rrr.ts +++ b/lib/backend-id/src/rrrs/user-field.rrr.ts @@ -1,19 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { RRR } from "@ordo-pink/core" import { type TIntake } from "@ordo-pink/routary" -import { type TSharedContext } from "../backend-id.types" +import { type TIDContext } from "../backend-id.types" -export const invalid_installed_functions_rrr = (installed_functions: unknown, intake: TIntake) => ({ +export const invalid_installed_functions_rrr = (installed_functions: unknown, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid installed functions", installed_functions), intake, }) -export const invalid_first_name_rrr = (first_name: unknown, intake: TIntake) => ({ +export const invalid_first_name_rrr = (first_name: unknown, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid first name", first_name), intake, }) -export const invalid_last_name_rrr = (last_name: unknown, intake: TIntake) => ({ +export const invalid_last_name_rrr = (last_name: unknown, intake: TIntake) => ({ rrr: RRR.codes.einval("invalid last name", last_name), intake, }) diff --git a/lib/backend-pb/index.ts b/lib/backend-pb/index.ts new file mode 100644 index 000000000..8e91ba02c --- /dev/null +++ b/lib/backend-pb/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/backend-pb.impl" +export * from "./src/backend-pb.types" diff --git a/lib/backend-pb/license b/lib/backend-pb/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/backend-pb/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/backend-pb/readme.md b/lib/backend-pb/readme.md new file mode 100644 index 000000000..4b008e0b0 --- /dev/null +++ b/lib/backend-pb/readme.md @@ -0,0 +1 @@ +# Backend Pb diff --git a/lib/backend-pb/src/backend-pb.impl.ts b/lib/backend-pb/src/backend-pb.impl.ts new file mode 100644 index 000000000..ea9b7cbb4 --- /dev/null +++ b/lib/backend-pb/src/backend-pb.impl.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { CurrentUser, Metadata, RRR } from "@ordo-pink/core" +import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { Routary, TIntake } from "@ordo-pink/routary" +import { create_json_response, create_response, status_from_rrr } from "@ordo-pink/backend-util-create-response" +import { set_x_response_time_header, start_response_timer, stop_response_timer } from "@ordo-pink/backend-util-response-time" +import { extract_request_ip } from "@ordo-pink/backend-util-extract-request-ip" +import { log_request } from "@ordo-pink/backend-util-log-request" +import { routary_cors } from "@ordo-pink/routary-cors" +import { set_content_type_application_json_header } from "@ordo-pink/backend-util-set-header" + +import { type TPBChamber, type TPBContext } from "./backend-pb.types" + +// TODO Extract colonoscope from Routary +// TODO WebSocket for dt-dt and dt-web notifications +export const create_backend_pb = (chamber: TPBChamber) => + Routary.Of({ ...chamber, headers: new Headers(), request_ip: null, status: 200 }) + .head("/:uid/:fsid", intake => { + const context = { ...intake, status: 204, request_ip: null, headers: intake.headers ?? new Headers() } + + return Oath.Resolve(context) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.chain(validate_request_params)) + .pipe(ops0.map(extract_ids(context))) + .pipe(ops0.chain(check_file_exists(context))) + .pipe(ops0.chain(set_last_modified_header(context))) + .pipe(ops0.map(() => context)) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake: context }))) + .fix(status_from_rrr) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.map(create_response)) + .invoke(invokers0.force_resolve) + }) + + .get("/:uid/:fsid", intake => { + const context = { ...intake, status: 200, request_ip: null, headers: intake.headers ?? new Headers() } + + return Oath.Resolve(context) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.chain(validate_request_params)) + .pipe(ops0.map(extract_ids(context))) + .pipe(ops0.chain(check_file_exists(context))) + .pipe(ops0.chain(set_last_modified_header(context))) + .pipe(ops0.chain(({ uid, fsid }) => context.data_persistence_strategy.read(uid, fsid))) + .pipe(ops0.tap(file => void (context.payload = file))) + .pipe(ops0.map(() => context)) + .pipe(ops0.tap(context => context.headers.set("Content-Type", "application/octet-stream"))) + .pipe(ops0.rejected_map(rrr => ({ rrr, intake: context }))) + .fix(status_from_rrr) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.map(create_response)) + .invoke(invokers0.force_resolve) + }) + + .get("/healthcheck", () => new Response("OK")) + + .use(routary_cors({ allow_origin: chamber.allow_origin, allow_headers: ["content-type", "authorization"] })) + + .start(intake => + Oath.Resolve>({ ...intake, headers: new Headers(), status: 404, request_ip: null }) + .pipe(ops0.tap(start_response_timer)) + .pipe(ops0.tap(extract_request_ip)) + .pipe(ops0.tap(set_content_type_application_json_header)) + .pipe(ops0.tap(stop_response_timer)) + .pipe(ops0.tap(set_x_response_time_header)) + .pipe(ops0.tap(log_request)) + .pipe(ops0.tap(intake => void (intake.payload = "resource not found"))) + .pipe(ops0.map(create_json_response)) + .invoke(invokers0.force_resolve), + ) + +const validate_request_params = (intake: TIntake) => + Oath.Merge([ + Oath.If(Metadata.Validations.is_fsid(intake.params.fsid)).pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid FSID"))), + Oath.If(CurrentUser.Validations.is_id(intake.params.uid)).pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid UID"))), + ]).pipe(ops0.map(() => intake)) + +const check_file_exists = + (intake: TIntake) => + ({ uid, fsid }: TIDs) => + intake.data_persistence_strategy + .exists(uid, fsid) + .pipe(ops0.chain(exists => Oath.If(exists))) + .pipe(ops0.map(() => ({ uid, fsid }))) + .pipe(ops0.rejected_map(() => RRR.codes.enoent("File not found"))) + +type TIDs = { uid: Ordo.User.UID; fsid: Ordo.Metadata.FSID } +const extract_ids = (intake: TIntake) => () => ({ + uid: intake.params.uid as Ordo.User.UID, + fsid: intake.params.fsid as Ordo.Metadata.FSID, +}) + +const set_last_modified_header = + (intake: TIntake) => + ({ uid, fsid }: TIDs) => + intake.data_persistence_strategy + .mtime(uid, fsid) + .pipe(ops0.map(mtime => new Date(mtime))) + .pipe(ops0.map(date => date.toUTCString())) + .pipe(ops0.tap(last_modified => intake.headers.set("Last-Modified", last_modified))) + .pipe(ops0.map(() => ({ uid, fsid }))) diff --git a/lib/backend-pb/src/backend-pb.test.ts b/lib/backend-pb/src/backend-pb.test.ts new file mode 100644 index 000000000..db19f5554 --- /dev/null +++ b/lib/backend-pb/src/backend-pb.test.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { expect, test } from "bun:test" +import { backend_pb } from "./backend-pb.impl" + +test("backend-pb should pass", () => { + expect(backend_pb).toEqual("backend-pb") +}) diff --git a/lib/backend-pb/src/backend-pb.types.ts b/lib/backend-pb/src/backend-pb.types.ts new file mode 100644 index 000000000..c8e41fbec --- /dev/null +++ b/lib/backend-pb/src/backend-pb.types.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import type { TDefaultContext } from "@ordo-pink/backend-util-default-handler" +import type { TLogger } from "@ordo-pink/logger" + +export type TPBChamber = { + allow_origin: string[] + data_persistence_strategy: OrdoBackend.Data.PersistenceStrategy + logger: TLogger +} + +export type TPBContext = TDefaultContext & TPBChamber diff --git a/lib/backend-persistence-strategy-content-fs/index.ts b/lib/backend-persistence-strategy-content-fs/index.ts deleted file mode 100644 index a4bef5b69..000000000 --- a/lib/backend-persistence-strategy-content-fs/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export * from "./src/backend-persistence-strategy-content-fs.impl" diff --git a/lib/backend-persistence-strategy-content-fs/readme.md b/lib/backend-persistence-strategy-content-fs/readme.md deleted file mode 100644 index ffd1bf658..000000000 --- a/lib/backend-persistence-strategy-content-fs/readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# Content persistence strategy FS - -`ContentPersistenceStrategyFS` implements the `ContentPersistenceStrategy` interface in a way that -enables file content persistence as files using file system. It only relies on streams so read/write -operations will not load the whole file into memory. - -## Warning - -This persistence strategy is not recommended for production usage. It is fine to use it on a -personal server with 3-5 people. It is also a viable option for development purposes. - -## Usage - -```typescript -import { createDataServer } from "@ordo-pink/backend-server-data" -import { ContentPersistenceStrategyFS } from "@ordo-pink/backend-content-persistence-strategy-fs" - -const root = "/var/ordo/content" // Path to where you want your files to be stored -const contentPersistenceStrategy = ContentPersistenceStrategyFS.of({ root }) - -const dataServer = createData({ contentPersistenceStrategy /* ...other things */ }) -``` diff --git a/lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.impl.ts b/lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.impl.ts deleted file mode 100644 index 0d59a40d7..000000000 --- a/lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.impl.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { Readable, Writable } from "stream" -import { createReadStream, createWriteStream } from "fs" -import { resolve } from "path" - -import { create_parent_if_not_exists0, removeFile0, stat0, write_file0 } from "@ordo-pink/fs" -import { Oath } from "@ordo-pink/oath" - -import { TPersistenceStrategyContentFSParams } from "./backend-persistence-strategy-content-fs.types" - -/** - * `ContentPersistenceStrategyFS` implements `ContentPersistenceStrategy` for storing content using - * file system. To create a `ContentPersistenceStrategyFS`, you need to provide the root directory - * where all the content will be stored. This strategy will automatically create the root directory - * if it does not exist. - * - * @warning This strategy is not intended to be used in production. - * - * @example - * const contentPersistenceStrategy = ContentPersistenceStrategyFS.of({ - * root: "/var/ordo/content", - * }) - */ -export const ContentPersistenceStrategyFS = { - /** - * `ContentPersistenceStrategyFS` factory. - */ - of: ({ root }: TPersistenceStrategyContentFSParams): ContentPersistenceStrategy => ({ - create: (uid, fsid) => - Oath.Resolve(getPath(root, uid, fsid)) - .pipe(chain_oath(createParentDirIfNotExists0)) - .pipe(chain_oath(createEmptyFileContent0)) - .pipe(map_oath(ok)), - - delete: (uid, fsid) => - Oath.Resolve(getPath(root, uid, fsid)) - .pipe(chain_oath(removeFile0)) - .pipe(bimap_oath(() => Data.Errors.DataNotFound, ok)), - - read: (uid, fsid) => Oath.Resolve(getPath(root, uid, fsid)).pipe(chain_oath(readFileContent0)), - - write: (uid, fsid, content) => - Oath.Resolve(getPath(root, uid, fsid)) - .pipe(chain_oath(createParentDirIfNotExists0)) - .pipe(chain_oath(writeFileContent0(content))) - .pipe(chain_oath(getFileSize0)), - }), -} - -// --- Internal --- - -const ok = () => "OK" as const - -const getPath = (root: string, uid: string, fsid: string): string => resolve(root, ...uid.split("-"), ...fsid.split("-")) - -const createParentDirIfNotExists0 = (path: string) => - create_parent_if_not_exists0(path).pipe(bimap_oath(UnexpectedError, () => path)) - -const getFileSize0 = (path: string) => stat0(path).pipe(bimap_oath(UnexpectedError, stat => Number(stat.size))) - -const createWriteStream0 = (path: string) => try_oath(() => createWriteStream(path, { autoClose: true })) - -const awaitStreamWriteCompleteP = (file: Writable, content: Readable) => - new Promise((resolve, reject) => { - file.on("finish", resolve) - file.on("error", reject) - content.on("error", reject) - content.pipe(file) - }) - -const readFileContent0 = (path: string) => - try_oath(() => createReadStream(path)).fix(() => { - const stream = new Readable() - stream.push("") - stream.push(null) - - return stream - }) - -const writeFileContent0 = (content: Readable) => (path: string) => - createWriteStream0(path) - .pipe(chain_oath(file => from_promise_oath(() => awaitStreamWriteCompleteP(file, content)))) - .pipe(bimap_oath(UnexpectedError, () => path)) - -const createEmptyFileContent0 = (path: string) => write_file0(path, "", "utf8").pipe(rejected_map_oath(UnexpectedError)) diff --git a/lib/backend-persistence-strategy-content-s3/index.ts b/lib/backend-persistence-strategy-content-s3/index.ts deleted file mode 100644 index 62fcec492..000000000 --- a/lib/backend-persistence-strategy-content-s3/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export * from "./src/backend-persistence-strategy-content-s3.impl" diff --git a/lib/backend-persistence-strategy-content-s3/license b/lib/backend-persistence-strategy-content-s3/license deleted file mode 100644 index 3a64ee5fe..000000000 --- a/lib/backend-persistence-strategy-content-s3/license +++ /dev/null @@ -1,504 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy -and distribute verbatim copies of this license document, but changing it is not allowed. - - Preamble - -The GNU Affero General Public License is a free, copyleft license for software and other kinds of -works, specifically designed to ensure cooperation with the community in the case of network server -software. - -The licenses for most software and other practical works are designed to take away your freedom to -share and change the works. By contrast, our General Public Licenses are intended to guarantee your -freedom to share and change all versions of a program--to make sure it remains free software for all -its users. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses -are designed to make sure that you have the freedom to distribute copies of free software (and -charge for them if you wish), that you receive source code or can get it if you want it, that you -can change the software or use pieces of it in new free programs, and that you know you can do these -things. - -Developers that use our General Public Licenses protect your rights with two steps: (1) assert -copyright on the software, and (2) offer you this License which gives you legal permission to copy, -distribute and/or modify the software. - -A secondary benefit of defending all users' freedom is that improvements made in alternate versions -of the program, if they receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and encouraged by the resulting -cooperation. However, in the case of software used on network servers, this result may fail to come -about. The GNU General Public License permits making a modified version and letting the public -access it on a server without ever releasing its source code to the public. - -The GNU Affero General Public License is designed specifically to ensure that, in such cases, the -modified source code becomes available to the community. It requires the operator of a network -server to provide the source code of the modified version running there to the users of that server. -Therefore, public use of a modified version, on a publicly accessible server, gives the public -access to the source code of the modified version. - -An older license, called the Affero General Public License and published by Affero, was designed to -accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero -has released a new version of the Affero GPL which permits relicensing under this license. - -The precise terms and conditions for copying, distribution and modification follow. - - TERMS AND CONDITIONS - -0. Definitions. - -"This License" refers to version 3 of the GNU Affero General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor -masks. - -"The Program" refers to any copyrightable work licensed under this License. Each licensee is -addressed as "you". "Licensees" and "recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring -copyright permission, other than the making of an exact copy. The resulting work is called a -"modified version" of the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the Program. - -To "propagate" a work means to do anything with it that, without permission, would make you directly -or secondarily liable for infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, distribution (with or without -modification), making available to the public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other parties to make or receive -copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a -convenient and prominently visible feature that (1) displays an appropriate copyright notice, and -(2) tells the user that there is no warranty for the work (except to the extent that warranties are -provided), that licensees may convey the work under this License, and how to view a copy of this -License. If the interface presents a list of user commands or options, such as a menu, a prominent -item in the list meets this criterion. - -1. Source Code. - -The "source code" for a work means the preferred form of the work for making modifications to it. -"Object code" means any non-source form of a work. - -A "Standard Interface" means an interface that either is an official standard defined by a -recognized standards body, or, in the case of interfaces specified for a particular programming -language, one that is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other than the work as a whole, that -(a) is included in the normal form of packaging a Major Component, but which is not part of that -Major Component, and (b) serves only to enable use of the work with that Major Component, or to -implement a Standard Interface for which an implementation is available to the public in source code -form. A "Major Component", in this context, means a major essential component (kernel, window -system, and so on) of the specific operating system (if any) on which the executable work runs, or a -compiler used to produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source code needed to -generate, install, and (for an executable work) run the object code and to modify the work, -including scripts to control those activities. However, it does not include the work's System -Libraries, or general-purpose tools or generally available free programs which are used unmodified -in performing those activities but which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for the work, and the source code -for shared libraries and dynamically linked subprograms that the work is specifically designed to -require, such as by intimate data communication or control flow between those subprograms and other -parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from -other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. - -All rights granted under this License are granted for the term of copyright on the Program, and are -irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a covered work is covered by this -License only if the output, given its content, constitutes a covered work. This License acknowledges -your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as -your license otherwise remains in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you with facilities for running -those works, provided that you comply with the terms of this License in conveying all material for -which you do not control copyright. Those thus making or running the covered works for you must do -so exclusively on your behalf, under your direction and control, on terms that prohibit them from -making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. -Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological measure under any applicable law -fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological -measures to the extent such circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit operation or modification of -the work as a means of enforcing, against the work's users, your or third parties' legal rights to -forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you receive it, in any medium, -provided that you conspicuously and appropriately publish on each copy an appropriate copyright -notice; keep intact all notices stating that this License and any non-permissive terms added in -accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and -give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or -warranty protection for a fee. - -5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to produce it from the Program, in -the form of source code under the terms of section 4, provided that you also meet all of these -conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their -nature extensions of the covered work, and which are not combined with it such as to form a larger -program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the -compilation and its resulting copyright are not used to limit the access or legal rights of the -compilation's users beyond what the individual works permit. Inclusion of a covered work in an -aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that -you also convey the machine-readable Corresponding Source under the terms of this License, in one of -these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source -as a System Library, need not be included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible personal property -which is normally used for personal, family, or household purposes, or (2) anything designed or sold -for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful -cases shall be resolved in favor of coverage. For a particular product received by a particular -user, "normally used" refers to a typical or common use of that class of product, regardless of the -status of the particular user or of the way in which the particular user actually uses, or expects -or is expected to use, the product. A product is a consumer product regardless of whether the -product has substantial commercial, industrial or non-consumer uses, unless such uses represent the -only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, authorization keys, or -other information required to install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The information must suffice to ensure -that the continued functioning of the modified object code is in no case prevented or interfered -with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User -Product, and the conveying occurs as part of a transaction in which the right of possession and use -of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of -how the transaction is characterized), the Corresponding Source conveyed under this section must be -accompanied by the Installation Information. But this requirement does not apply if neither you nor -any third party retains the ability to install modified object code on the User Product (for -example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to -provide support service, warranty, or updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or installed. Access to a network -may be denied when the modification itself materially and adversely affects the operation of the -network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section -must be in a format that is publicly documented (and with an implementation available to the public -in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this License by making exceptions -from one or more of its conditions. Additional permissions that are applicable to the entire Program -shall be treated as though they were included in this License, to the extent that they are valid -under applicable law. If additional permissions apply only to part of the Program, that part may be -used separately under those permissions, but the entire Program remains governed by this License -without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions -from that copy, or from any part of it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place additional permissions on -material, added by you to a covered work, for which you have or can give appropriate copyright -permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may -(if authorized by the copyright holders of that material) supplement the terms of this License with -terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - -All other non-permissive additional terms are considered "further restrictions" within the meaning -of section 10. If the Program as you received it, or any part of it, contains a notice stating that -it is governed by this License along with a term that is a further restriction, you may remove that -term. If a license document contains a further restriction but permits relicensing or conveying -under this License, you may add to a covered work material governed by the terms of that license -document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant -source files, a statement of the additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written -license, or stated as exceptions; the above requirements apply either way. - -8. Termination. - -You may not propagate or modify a covered work except as expressly provided under this License. Any -attempt otherwise to propagate or modify it is void, and will automatically terminate your rights -under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright -holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder fails to notify you of the -violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright -holder notifies you of the violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that copyright holder, and you cure -the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have -received copies or rights from you under this License. If your rights have been terminated and not -permanently reinstated, you do not qualify to receive new licenses for the same material under -section 10. - -9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run a copy of the Program. -Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer -transmission to receive a copy likewise does not require acceptance. However, nothing other than -this License grants you permission to propagate or modify any covered work. These actions infringe -copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, -you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically receives a license from the -original licensors, to run, modify and propagate that work, subject to this License. You are not -responsible for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an organization, or substantially -all assets of one, or subdividing an organization, or merging organizations. If propagation of a -covered work results from an entity transaction, each party to that transaction who receives a copy -of the work also receives whatever licenses to the work the party's predecessor in interest had or -could give under the previous paragraph, plus a right to possession of the Corresponding Source of -the work from the predecessor in interest, if the predecessor has it or can get it with reasonable -efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under -this License. For example, you may not impose a license fee, royalty, or other charge for exercise -of rights granted under this License, and you may not initiate litigation (including a cross-claim -or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, -offering for sale, or importing the Program or any portion of it. - -11. Patents. - -A "contributor" is a copyright holder who authorizes use under this License of the Program or a work -on which the Program is based. The work thus licensed is called the contributor's "contributor -version". - -A contributor's "essential patent claims" are all patent claims owned or controlled by the -contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, -permitted by this License, of making, using, or selling its contributor version, but do not include -claims that would be infringed only as a consequence of further modification of the contributor -version. For purposes of this definition, "control" includes the right to grant patent sublicenses -in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the -contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, -modify and propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement or commitment, -however denominated, not to enforce a patent (such as an express permission to practice a patent or -covenant not to sue for patent infringement). To "grant" such a patent license to a party means to -make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of -the work is not available for anyone to copy, free of charge and under the terms of this License, -through a publicly available network server or other readily accessible means, then you must either -(1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the -benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with -the requirements of this License, to extend the patent license to downstream recipients. "Knowingly -relying" means you have actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work in a country, would infringe -one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate -by procuring conveyance of, a covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of -the covered work, then the patent license you grant is automatically extended to all recipients of -the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of its coverage, -prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that -are specifically granted under this License. You may not convey a covered work if you are a party to -an arrangement with a third party that is in the business of distributing software, under which you -make payment to the third party based on the extent of your activity of conveying the work, and -under which the third party grants, to any of the parties who would receive the covered work from -you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in connection with specific -products or compilations that contain the covered work, unless you entered into that arrangement, or -that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other -defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict -the conditions of this License, they do not excuse you from the conditions of this License. If you -cannot convey a covered work so as to satisfy simultaneously your obligations under this License and -any other pertinent obligations, then as a consequence you may not convey it at all. For example, if -you agree to terms that obligate you to collect a royalty for further conveying from those to whom -you convey the Program, the only way you could satisfy both those terms and this License would be to -refrain entirely from conveying the Program. - -13. Remote Network Interaction; Use with the GNU General Public License. - -Notwithstanding any other provision of this License, if you modify the Program, your modified -version must prominently offer all users interacting with it remotely through a computer network (if -your version supports such interaction) an opportunity to receive the Corresponding Source of your -version by providing access to the Corresponding Source from a network server at no charge, through -some standard or customary means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 of the GNU General Public -License that is incorporated pursuant to the following paragraph. - -Notwithstanding any other provision of this License, you have permission to link or combine any -covered work with a work licensed under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this License will continue to apply to -the part which is the covered work, but the work with which it is combined will remain governed by -version 3 of the GNU General Public License. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of the GNU Affero General -Public License from time to time. Such new versions will be similar in spirit to the present -version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain -numbered version of the GNU Affero General Public License "or any later version" applies to it, you -have the option of following the terms and conditions either of that numbered version or of any -later version published by the Free Software Foundation. If the Program does not specify a version -number of the GNU Affero General Public License, you may choose any version ever published by the -Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU Affero General -Public License can be used, that proxy's public statement of acceptance of a version permanently -authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional -obligations are imposed on any author or copyright holder as a result of your choosing to follow a -later version. - -15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" -WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO -THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU -ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR -ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR -DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE -OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED -INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH -ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - -17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal -effect according to their terms, reviewing courts shall apply local law that most closely -approximates an absolute waiver of all civil liability in connection with the Program, unless a -warranty or assumption of liability accompanies a copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS diff --git a/lib/backend-persistence-strategy-content-s3/readme.md b/lib/backend-persistence-strategy-content-s3/readme.md deleted file mode 100644 index c0350b4f7..000000000 --- a/lib/backend-persistence-strategy-content-s3/readme.md +++ /dev/null @@ -1 +0,0 @@ -# Backend Content Persistence Strategy S3 diff --git a/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.impl.ts b/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.impl.ts deleted file mode 100644 index e353b5f0b..000000000 --- a/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.impl.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { DeleteObjectCommand, GetObjectCommand, S3Client } from "@aws-sdk/client-s3" -import { PassThrough, Readable } from "stream" -import { Upload } from "@aws-sdk/lib-storage" - -import { ContentPersistenceStrategy, Data, FSID, UnexpectedError, UserID } from "@ordo-pink/managers" -import { Oath } from "@ordo-pink/oath" -import { bimap_oath } from "@ordo-pink/oath/operators/bimap" -import { chain_oath } from "@ordo-pink/oath/operators/chain" -import { from_promise_oath } from "@ordo-pink/oath/constructors/from-promise" -import { map_oath } from "@ordo-pink/oath/operators/map" -import { rejected_map_oath } from "@ordo-pink/oath/operators/rejected-map" -import { tap_oath } from "@ordo-pink/oath/operators/tap" - -import { S3DownloadStream } from "./s3-download-stream" -import { type TPersistenceStrategyContentS3Params } from "./backend-persistence-strategy-content-s3.types" - -/** - * `ContentPersistenceStrategyS3` implements `ContentPersistenceStrategy` for storing content using - * AWS S3. To create a `ContentPersistenceStrategyS3`. - * - * @example - * const contentPersistenceStrategy = ContentPersistenceStrategyFS.of({ - * accessKeyId: "YOUR_ACCESS_KEY", - * secretAccessKey: "YOUR_SECRET_KEY", - * region: "us-east-1", - * endpoint: "YOUR_AWS_ENDPOINT", - * bucketName: "content-test-bucket" - * }) - */ -export const ContentPersistenceStrategyS3 = { - /** - * `ContentPersistenceStrategyS3` factory. - */ - of: ({ - accessKeyId, - secretAccessKey, - region, - endpoint, - bucketName, - }: TPersistenceStrategyContentS3Params): ContentPersistenceStrategy => { - const s3 = new S3Client({ region, endpoint, credentials: { accessKeyId, secretAccessKey } }) - - return { - create: () => Oath.Resolve("OK"), - - delete: (uid, fsid) => - Oath.Resolve(getKey(uid, fsid)) - .pipe(chain_oath(s3DeleteObject0(s3, bucketName))) - .pipe(bimap_oath(dataNotFound, ok)), - - read: (uid, fsid) => - Oath.Resolve(getKey(uid, fsid)) - .pipe(map_oath(s3ReadObject0(s3, bucketName))) - .fix(getEmptyReadableStream), - - write: (uid, fsid, content, length) => - Oath.Resolve(getKey(uid, fsid)) - .pipe(chain_oath(s3WriteObject0(s3, bucketName, content))) - .pipe(map_oath(() => length)), - } - }, -} - -// --- Internal --- - -const ok = () => "OK" as const - -const dataNotFound = () => Data.Errors.DataNotFound - -const getEmptyReadableStream = () => Readable.from([""]) - -const s3DeleteObject0 = (s3: S3Client, Bucket: string) => (Key: string) => - from_promise_oath(() => s3.send(new DeleteObjectCommand({ Bucket, Key }))) - -const s3ReadObject0 = (s3: S3Client, Bucket: string) => (Key: string) => new S3DownloadStream({ Key, Bucket, s3 }) - -const s3WriteObject0 = (s3: S3Client, Bucket: string, content: Readable) => (Key: string) => - Oath.Resolve(new PassThrough()) - .pipe(tap_oath(stream => content.pipe(stream))) - .pipe(map_oath(Body => new Upload({ client: s3, params: { Bucket, Key, Body } }))) - .pipe(chain_oath(upload => from_promise_oath(() => upload.done()))) - .pipe(chain_oath(() => from_promise_oath(() => s3.send(new GetObjectCommand({ Bucket, Key }))))) - .pipe(rejected_map_oath(UnexpectedError)) - -/** - * Creates a bucket object key from user id and fs id. - */ -const getKey = (uid: UserID, fsid: FSID) => `${uid}/${fsid}` diff --git a/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.types.ts b/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.types.ts deleted file mode 100644 index df7c987e2..000000000 --- a/lib/backend-persistence-strategy-content-s3/src/backend-persistence-strategy-content-s3.types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export type TPersistenceStrategyContentS3Params = { - accessKeyId: string - secretAccessKey: string - region: string - bucketName: string - endpoint?: string -} diff --git a/lib/backend-persistence-strategy-content-s3/src/s3-download-stream.ts b/lib/backend-persistence-strategy-content-s3/src/s3-download-stream.ts deleted file mode 100644 index b69482592..000000000 --- a/lib/backend-persistence-strategy-content-s3/src/s3-download-stream.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { GetObjectCommand, HeadObjectCommand, S3Client } from "@aws-sdk/client-s3" -import { ReadableOptions, Stream, Transform, TransformCallback } from "stream" - -type S3DownloadStreamOptions = { - readonly s3: S3Client - readonly Bucket: string - readonly Key: string - readonly byteRange?: number -} - -const DEFAULT_CHUNK_SIZE = 512 * 1024 - -export class S3DownloadStream extends Transform { - #options: S3DownloadStreamOptions - #currentCursorPosition = 0 - #maxContentLength = -1 - - constructor(options: S3DownloadStreamOptions, nodeReadableStreamOptions?: ReadableOptions) { - super(nodeReadableStreamOptions) - this.#options = options - void this.init() - } - - async init() { - try { - const res = await this.#options.s3.send(new HeadObjectCommand({ Bucket: this.#options.Bucket, Key: this.#options.Key })) - this.#maxContentLength = res.ContentLength || 0 - } catch (e) { - this.destroy(e as Error) - return - } - - await this.fetchAndEmitNextRange() - } - - async fetchAndEmitNextRange() { - if (this.#currentCursorPosition >= this.#maxContentLength) { - this.end() - return - } - - // Calculate the range of bytes we want to grab - const range = this.#currentCursorPosition + (this.#options.byteRange ?? DEFAULT_CHUNK_SIZE) - - // If the range is greater than the total number of bytes in the file - // We adjust the range to grab the remaining bytes of data - const adjustedRange = range < this.#maxContentLength ? range : this.#maxContentLength - - // Set the Range property on our s3 stream parameters - const Range = `bytes=${this.#currentCursorPosition}-${adjustedRange}` - const { Key, Bucket } = this.#options - - // Update the current range beginning for the next go - this.#currentCursorPosition = adjustedRange + 1 - - try { - // Grab the range of bytes from the file - const res = await this.#options.s3.send(new GetObjectCommand({ Bucket, Key, Range })) - - const data = res?.Body || "" - - if (!(data instanceof Stream.Readable)) { - // never encountered this error, but you never know - this.destroy(new Error(`Unsupported data representation: ${data as string}`)) - return - } - - data.pipe(this, { end: false }) - - let streamClosed = false - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - data.on("end", async () => { - if (streamClosed) { - return - } - - streamClosed = true - await this.fetchAndEmitNextRange() - }) - } catch (error) { - // If we encounter an error grabbing the bytes - // We destroy the stream, NodeJS ReadableStream will emit the 'error' event - this.destroy(error as Error) - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override _transform(chunk: any, _: BufferEncoding, callback: TransformCallback) { - callback(null, chunk) - } -} diff --git a/lib/backend-persistence-strategy-data-bun-s3/index.ts b/lib/backend-persistence-strategy-data-bun-s3/index.ts new file mode 100644 index 000000000..60fefed05 --- /dev/null +++ b/lib/backend-persistence-strategy-data-bun-s3/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/backend-persistence-strategy-data-bun-s3.impl" +export * from "./src/backend-persistence-strategy-data-bun-s3.types" diff --git a/lib/backend-persistence-strategy-data-bun-s3/license b/lib/backend-persistence-strategy-data-bun-s3/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/backend-persistence-strategy-data-bun-s3/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/backend-persistence-strategy-data-bun-s3/readme.md b/lib/backend-persistence-strategy-data-bun-s3/readme.md new file mode 100644 index 000000000..0fea2d7f0 --- /dev/null +++ b/lib/backend-persistence-strategy-data-bun-s3/readme.md @@ -0,0 +1,28 @@ +# S3 Persistence Strategy for User Data + +This strategy implements storing user data in S3. The strategy is used by `backend-dt` for storing user files in S3. + +NOTE: This implementation relies on Bun S3 client. Make sure you run your `backend-dt` instance with Bun to make it work +properly. + +## Installation + +```sh +bunx jsr add @ordo-pink/backend-persistence-strategy-data-s3 @ordo-pink/oath @ordo-pink/tau @ordo-pink/core +``` + +## Usage + +```typescript +import { PersistenceStrategyDataS3 } from "@ordo-pink/backend-persistence-strategy-data-s3" + +const data_persistence_strategy = PersistenceStrategyDataS3.Of({ + access_key: process.env.S3_ACCESS_KEY_ID, + bucket: process.env.S3_BUCKET_NAME, + endpoint: process.env.S3_ENDPOINT, + region: process.env.S3_REGION, + secret_key: process.env.S3_SECRET_ACCESS_KEY, +}) + +// Provide `data_persistence_strategy` to `create_backend_dt`. See more in "@ordo-pink/backend-dt". +``` diff --git a/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.impl.ts b/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.impl.ts new file mode 100644 index 000000000..f503e0286 --- /dev/null +++ b/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.impl.ts @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { S3Client, S3File } from "bun" + +import { Oath, ops0 } from "@ordo-pink/oath" +import { RRR } from "@ordo-pink/core" +import { prop } from "@ordo-pink/tau" + +import { type TPersistenceStrategyDataS3 } from "./backend-persistence-strategy-data-bun-s3.types" + +/** + * `PersistenceStrategyDataS3` implements `OrdoBackend.Data.PersistenceStrategy` for storing + * data using AWS S3. + * + * @example + * const data_ps = PersistenceStrategyDataS3.Of({ + * access_key: "YOUR_ACCESS_KEY", + * bucket: "content-test-bucket" + * endpoint: "YOUR_AWS_ENDPOINT", + * region: "us-east-1", + * secret_key: "YOUR_SECRET_KEY", + * }) + */ +export const PersistenceStrategyDataS3: TPersistenceStrategyDataS3 = { + Of: ({ access_key, bucket, endpoint, region, secret_key }) => { + const s3 = new Bun.S3Client({ accessKeyId: access_key, bucket, endpoint, region, secretAccessKey: secret_key }) + + return { + exists: (uid, fsid) => + get_key(uid, fsid) + .pipe(ops0.chain(check_file_exists(s3))) + .pipe(ops0.map(prop("exists"))), + + create: (uid, fsid, content) => + get_key(uid, fsid) + .pipe(ops0.chain(validate_file_does_not_exist(s3))) + .pipe(ops0.map(prop("path"))) + .pipe(ops0.chain(write_file(s3, content))), + + read: (uid, fsid) => + get_key(uid, fsid) + .pipe(ops0.chain(validate_file_exists(s3))) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.chain(get_file_content)), + + update: (uid, fsid, content) => + get_key(uid, fsid) + .pipe(ops0.chain(get_file(s3))) + .pipe(ops0.map(prop("path"))) + .pipe(ops0.chain(write_file(s3, content))), + + delete: (uid, fsid) => + get_key(uid, fsid) + .pipe(ops0.chain(validate_file_exists(s3))) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.chain(delete_file)), + + mtime: (uid, fsid) => + get_key(uid, fsid) + .pipe(ops0.chain(validate_file_exists(s3))) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.chain(get_file_modification_timestamp)), + } + }, +} + +// --- Internal --- + +const already_exists_rrr = () => RRR.codes.eexist("File already exists") +const not_found_rrr = () => RRR.codes.enoent("File not found") +const io_rrr = (e: Error) => RRR.codes.eio(e.message) + +const get_file = (s3: S3Client) => (path: string) => + Oath.Try(() => s3.file(path)) + .pipe(ops0.map(file => ({ path, file }))) + .pipe(ops0.rejected_map(io_rrr)) + +const check_file_exists = (s3: S3Client) => (path: string) => + Oath.Resolve(get_file(s3)) + .pipe(ops0.chain(f => f(path))) + .pipe(ops0.map(prop("file"))) + .pipe( + ops0.chain(file => + Oath.FromPromise(() => file.exists()) + .fix(() => false) + .pipe(ops0.map(exists => ({ file, exists }))), + ), + ) + +const validate_file_exists = (s3: S3Client) => (path: string) => + Oath.Resolve(check_file_exists(s3)) + .pipe(ops0.chain(f => f(path))) + .pipe( + ops0.chain(({ exists, file }) => + Oath.If(exists) + .pipe(ops0.rejected_map(not_found_rrr)) + .pipe(ops0.map(() => ({ path, file }))), + ), + ) + +const validate_file_does_not_exist = (s3: S3Client) => (path: string) => + Oath.Resolve(check_file_exists(s3)) + .pipe(ops0.chain(f => f(path))) + .pipe( + ops0.chain(({ exists, file }) => + Oath.If(!exists) + .pipe(ops0.rejected_map(already_exists_rrr)) + .pipe(ops0.map(() => ({ path, file }))), + ), + ) + +const write_file = (s3: S3Client, content: ReadableStream) => (path: string) => + Oath.Try(() => new Response(content)) + .pipe(ops0.chain(data => Oath.FromPromise(() => s3.write(path, data)))) + .pipe(ops0.rejected_map(io_rrr)) + +const delete_file = (file: S3File) => Oath.Try(() => file.delete()).pipe(ops0.rejected_map(io_rrr)) + +const get_file_modification_timestamp = (file: S3File) => + Oath.FromPromise(() => file.stat()) + .pipe(ops0.map(stat => stat.lastModified.getTime())) + .pipe(ops0.map(milliseconds => milliseconds / 1000)) + .pipe(ops0.map(Math.floor)) + .pipe(ops0.rejected_map(e => RRR.codes.eio(e.message))) + +const get_file_content = (file: S3File) => Oath.Try(() => file.readable).pipe(ops0.rejected_map(io_rrr)) + +const get_key = (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath.Resolve(`${uid}/${fsid}`) diff --git a/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.types.ts b/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.types.ts new file mode 100644 index 000000000..a6f233a19 --- /dev/null +++ b/lib/backend-persistence-strategy-data-bun-s3/src/backend-persistence-strategy-data-bun-s3.types.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export type TPersistenceStrategyDataS3Params = { + access_key: string + secret_key: string + region: string + bucket: string + endpoint?: string +} + +export type TPersistenceStrategyDataS3 = { + Of: (params: TPersistenceStrategyDataS3Params) => OrdoBackend.Data.PersistenceStrategy +} diff --git a/lib/backend-persistence-strategy-data-fs/index.ts b/lib/backend-persistence-strategy-data-fs/index.ts new file mode 100644 index 000000000..5e694efd2 --- /dev/null +++ b/lib/backend-persistence-strategy-data-fs/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/backend-persistence-strategy-data-fs.impl" +export * from "./src/backend-persistence-strategy-data-fs.types" diff --git a/lib/backend-persistence-strategy-data-fs/license b/lib/backend-persistence-strategy-data-fs/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/backend-persistence-strategy-data-fs/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/backend-persistence-strategy-data-fs/readme.md b/lib/backend-persistence-strategy-data-fs/readme.md new file mode 100644 index 000000000..c5f176d0f --- /dev/null +++ b/lib/backend-persistence-strategy-data-fs/readme.md @@ -0,0 +1,26 @@ +# FS Persistence Strategy for User Data + +`ContentPersistenceStrategyFS` implements the `ContentPersistenceStrategy` interface in a way that enables file content +persistence as files using file system. It only relies on streams so read/write operations will not load the whole file into +memory. + +## Warning + +This persistence strategy is not recommended for production usage. It is fine to use it on a personal server with 3-5 people. It +is also a viable option for development purposes. + +## Installation + +```sh +bunx jsr add @ordo-pink/backend-persistence-strategy-data-fs @ordo-pink/oath @ordo-pink/tau @ordo-pink/core +``` + +## Usage + +```typescript +import { PersistenceStrategyDataFS } from "@ordo-pink/backend-persistence-strategy-data-fs" + +const contentPersistenceStrategy = ContentPersistenceStrategyFS.of("/var/ordo/files") + +// Provide `data_persistence_strategy` to `create_backend_dt`. See more in "@ordo-pink/backend-dt". +``` diff --git a/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.impl.ts b/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.impl.ts new file mode 100644 index 000000000..04d5b0b04 --- /dev/null +++ b/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.impl.ts @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { BunFile } from "bun" +import { resolve } from "path" + +import { Oath, ops0 } from "@ordo-pink/oath" +import { RRR } from "@ordo-pink/core" +import { prop } from "@ordo-pink/tau" + +import { TPersistenceStategyDataFS } from "./backend-persistence-strategy-data-fs.types" + +/** + * `PersistenceStrategyDataFS` implements `OrdoBackend.Data.PersistenceStrategy` for storing data + * using file system. To create a `PersistenceStrategyDataFS`, you need to provide the root directory + * where all the content will be stored. This strategy will automatically create the root directory + * if it does not exist. + * + * @warning This strategy is not intended to be used in production. + * + * @example + * const data_ps = PersistenceStrategyDataFS.of("/var/dt/files") + */ +export const PersistenceStrategyDataFS: TPersistenceStategyDataFS = { + Of: root => { + const get_path = get_path_from_root(root) + + return { + exists: (uid, fsid) => + get_path(uid, fsid) + .pipe(ops0.chain(check_file_exists)) + .pipe(ops0.map(prop("exists"))), + + create: (uid, fsid, content) => + get_path(uid, fsid) + .pipe(ops0.chain(validate_file_does_not_exist)) + .pipe(ops0.map(prop("path"))) + .pipe(ops0.chain(write_file(content))), + + read: (uid, fsid) => + get_path(uid, fsid) + .pipe(ops0.chain(validate_file_exists)) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.chain(get_file_content)), + + update: (uid, fsid, content) => + get_path(uid, fsid) + .pipe(ops0.chain(get_file)) + .pipe(ops0.chain(write_file(content))), + + delete: (uid, fsid) => + get_path(uid, fsid) + .pipe(ops0.chain(validate_file_exists)) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.chain(delete_file)), + + mtime: (uid, fsid) => + get_path(uid, fsid) + .pipe(ops0.chain(validate_file_exists)) + .pipe(ops0.map(prop("file"))) + .pipe(ops0.map(file => file.lastModified)), + } + }, +} + +// --- Internal --- + +const already_exists_rrr = () => RRR.codes.eexist("File already exists") +const not_found_rrr = () => RRR.codes.enoent("File not found") +const io_rrr = (e: Error) => RRR.codes.eio(e.message) + +const get_file = (path: string) => Oath.Try(() => Bun.file(path)).pipe(ops0.rejected_map(io_rrr)) + +const check_file_exists = (path: string) => + get_file(path).pipe( + ops0.chain(file => + Oath.FromPromise(() => file.exists()) + .fix(() => false) + .pipe(ops0.map(exists => ({ file, exists }))), + ), + ) + +const write_file = (content: ReadableStream) => (path: BunFile | string) => + Oath.Try(() => Bun.readableStreamToArrayBuffer(content)) + .pipe(ops0.chain(input => Oath.FromPromise(() => Bun.write(path as BunFile, input)))) + .pipe(ops0.rejected_map(io_rrr)) + +const delete_file = (file: BunFile) => Oath.Try(() => file.delete()).pipe(ops0.rejected_map(io_rrr)) + +const validate_file_exists = (path: string) => + check_file_exists(path).pipe( + ops0.chain(({ exists, file }) => + Oath.If(exists) + .pipe(ops0.rejected_map(not_found_rrr)) + .pipe(ops0.map(() => ({ path, file }))), + ), + ) + +const validate_file_does_not_exist = (path: string) => + check_file_exists(path).pipe( + ops0.chain(({ exists, file }) => + Oath.If(!exists) + .pipe(ops0.rejected_map(already_exists_rrr)) + .pipe(ops0.map(() => ({ path, file }))), + ), + ) + +const get_file_content = (file: BunFile) => Oath.Try(() => file.stream()).pipe(ops0.rejected_map(io_rrr)) + +const get_path_from_root = (root: string) => (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => + Oath.Try(() => resolve(root, uid, ...fsid.split("-"))).pipe(ops0.rejected_map(io_rrr)) diff --git a/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.types.ts b/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.types.ts new file mode 100644 index 000000000..48156fa8b --- /dev/null +++ b/lib/backend-persistence-strategy-data-fs/src/backend-persistence-strategy-data-fs.types.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export type TPersistenceStrategyDataFSParams = { root: string } + +export type TPersistenceStategyDataFS = { + Of: (root: string) => OrdoBackend.Data.PersistenceStrategy +} diff --git a/lib/backend-persistence-strategy-token-fs/index.ts b/lib/backend-persistence-strategy-token-fs/index.ts index f48d092c3..b26fb0986 100644 --- a/lib/backend-persistence-strategy-token-fs/index.ts +++ b/lib/backend-persistence-strategy-token-fs/index.ts @@ -39,7 +39,10 @@ export const PersistenceStrategyTokenFS: TPersistenceStrategyTokenFSStatic = { .pipe(ops0.rejected_map(e => RRR.codes.eio(e.message, e.name, e.cause, e.stack))) return { - get_token: (sub, jti) => get_tokens0.pipe(ops0.map(storage => storage[sub]?.[jti])), + get_token: (sub, jti) => + get_tokens0.pipe( + ops0.chain(storage => Oath.FromNullable(storage[sub]?.[jti], () => RRR.codes.enoent("Token not found"))), + ), get_token_dict: sub => get_tokens0.pipe(ops0.map(storage => storage[sub])), diff --git a/lib/backend-persistence-strategy-user-dynamodb/src/backend-persistence-strategy-user-dynamodb.impl.ts b/lib/backend-persistence-strategy-user-dynamodb/src/backend-persistence-strategy-user-dynamodb.impl.ts index 4c0e5719b..5c2de1396 100644 --- a/lib/backend-persistence-strategy-user-dynamodb/src/backend-persistence-strategy-user-dynamodb.impl.ts +++ b/lib/backend-persistence-strategy-user-dynamodb/src/backend-persistence-strategy-user-dynamodb.impl.ts @@ -150,7 +150,7 @@ const _check_not_exists_by_handle0 = (params: T.TDynamoDBConfig, handle: Ordo.Us .exists_by_handle(handle) .pipe(ops0.chain(exists => Oath.If(!exists, { F: () => RRR.codes.eexist(`User already exists: handle ${handle}`) }))) -const _check_not_exists_by_id0 = (params: T.TDynamoDBConfig, id: Ordo.User.ID) => +const _check_not_exists_by_id0 = (params: T.TDynamoDBConfig, id: Ordo.User.UID) => PersistenceStrategyUserDynamoDB.Of(params) .exists_by_id(id) .pipe(ops0.chain(exists => Oath.If(!exists, { F: () => RRR.codes.eexist(`User already exists: id ${id}`) }))) @@ -201,7 +201,7 @@ const _deserialize: T.TDeserialiseFn = item => ({ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain email_code: item.email_code?.S!, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - id: item.id.S! as Ordo.User.ID, + id: item.id.S! as Ordo.User.UID, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain installed_functions: item.installed_functions.SS!, }) diff --git a/lib/backend-server-data/index.ts b/lib/backend-server-data/index.ts deleted file mode 100644 index b0fe7cbae..000000000 --- a/lib/backend-server-data/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -export * from "./src/backend-server-data.impl" diff --git a/lib/backend-server-data/license b/lib/backend-server-data/license deleted file mode 100644 index 3a64ee5fe..000000000 --- a/lib/backend-server-data/license +++ /dev/null @@ -1,504 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy -and distribute verbatim copies of this license document, but changing it is not allowed. - - Preamble - -The GNU Affero General Public License is a free, copyleft license for software and other kinds of -works, specifically designed to ensure cooperation with the community in the case of network server -software. - -The licenses for most software and other practical works are designed to take away your freedom to -share and change the works. By contrast, our General Public Licenses are intended to guarantee your -freedom to share and change all versions of a program--to make sure it remains free software for all -its users. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses -are designed to make sure that you have the freedom to distribute copies of free software (and -charge for them if you wish), that you receive source code or can get it if you want it, that you -can change the software or use pieces of it in new free programs, and that you know you can do these -things. - -Developers that use our General Public Licenses protect your rights with two steps: (1) assert -copyright on the software, and (2) offer you this License which gives you legal permission to copy, -distribute and/or modify the software. - -A secondary benefit of defending all users' freedom is that improvements made in alternate versions -of the program, if they receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and encouraged by the resulting -cooperation. However, in the case of software used on network servers, this result may fail to come -about. The GNU General Public License permits making a modified version and letting the public -access it on a server without ever releasing its source code to the public. - -The GNU Affero General Public License is designed specifically to ensure that, in such cases, the -modified source code becomes available to the community. It requires the operator of a network -server to provide the source code of the modified version running there to the users of that server. -Therefore, public use of a modified version, on a publicly accessible server, gives the public -access to the source code of the modified version. - -An older license, called the Affero General Public License and published by Affero, was designed to -accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero -has released a new version of the Affero GPL which permits relicensing under this license. - -The precise terms and conditions for copying, distribution and modification follow. - - TERMS AND CONDITIONS - -0. Definitions. - -"This License" refers to version 3 of the GNU Affero General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor -masks. - -"The Program" refers to any copyrightable work licensed under this License. Each licensee is -addressed as "you". "Licensees" and "recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring -copyright permission, other than the making of an exact copy. The resulting work is called a -"modified version" of the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the Program. - -To "propagate" a work means to do anything with it that, without permission, would make you directly -or secondarily liable for infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, distribution (with or without -modification), making available to the public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other parties to make or receive -copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a -convenient and prominently visible feature that (1) displays an appropriate copyright notice, and -(2) tells the user that there is no warranty for the work (except to the extent that warranties are -provided), that licensees may convey the work under this License, and how to view a copy of this -License. If the interface presents a list of user commands or options, such as a menu, a prominent -item in the list meets this criterion. - -1. Source Code. - -The "source code" for a work means the preferred form of the work for making modifications to it. -"Object code" means any non-source form of a work. - -A "Standard Interface" means an interface that either is an official standard defined by a -recognized standards body, or, in the case of interfaces specified for a particular programming -language, one that is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other than the work as a whole, that -(a) is included in the normal form of packaging a Major Component, but which is not part of that -Major Component, and (b) serves only to enable use of the work with that Major Component, or to -implement a Standard Interface for which an implementation is available to the public in source code -form. A "Major Component", in this context, means a major essential component (kernel, window -system, and so on) of the specific operating system (if any) on which the executable work runs, or a -compiler used to produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source code needed to -generate, install, and (for an executable work) run the object code and to modify the work, -including scripts to control those activities. However, it does not include the work's System -Libraries, or general-purpose tools or generally available free programs which are used unmodified -in performing those activities but which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for the work, and the source code -for shared libraries and dynamically linked subprograms that the work is specifically designed to -require, such as by intimate data communication or control flow between those subprograms and other -parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from -other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. - -All rights granted under this License are granted for the term of copyright on the Program, and are -irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a covered work is covered by this -License only if the output, given its content, constitutes a covered work. This License acknowledges -your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as -your license otherwise remains in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you with facilities for running -those works, provided that you comply with the terms of this License in conveying all material for -which you do not control copyright. Those thus making or running the covered works for you must do -so exclusively on your behalf, under your direction and control, on terms that prohibit them from -making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. -Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological measure under any applicable law -fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological -measures to the extent such circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit operation or modification of -the work as a means of enforcing, against the work's users, your or third parties' legal rights to -forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you receive it, in any medium, -provided that you conspicuously and appropriately publish on each copy an appropriate copyright -notice; keep intact all notices stating that this License and any non-permissive terms added in -accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and -give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or -warranty protection for a fee. - -5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to produce it from the Program, in -the form of source code under the terms of section 4, provided that you also meet all of these -conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their -nature extensions of the covered work, and which are not combined with it such as to form a larger -program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the -compilation and its resulting copyright are not used to limit the access or legal rights of the -compilation's users beyond what the individual works permit. Inclusion of a covered work in an -aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that -you also convey the machine-readable Corresponding Source under the terms of this License, in one of -these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source -as a System Library, need not be included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible personal property -which is normally used for personal, family, or household purposes, or (2) anything designed or sold -for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful -cases shall be resolved in favor of coverage. For a particular product received by a particular -user, "normally used" refers to a typical or common use of that class of product, regardless of the -status of the particular user or of the way in which the particular user actually uses, or expects -or is expected to use, the product. A product is a consumer product regardless of whether the -product has substantial commercial, industrial or non-consumer uses, unless such uses represent the -only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, authorization keys, or -other information required to install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The information must suffice to ensure -that the continued functioning of the modified object code is in no case prevented or interfered -with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User -Product, and the conveying occurs as part of a transaction in which the right of possession and use -of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of -how the transaction is characterized), the Corresponding Source conveyed under this section must be -accompanied by the Installation Information. But this requirement does not apply if neither you nor -any third party retains the ability to install modified object code on the User Product (for -example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to -provide support service, warranty, or updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or installed. Access to a network -may be denied when the modification itself materially and adversely affects the operation of the -network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section -must be in a format that is publicly documented (and with an implementation available to the public -in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this License by making exceptions -from one or more of its conditions. Additional permissions that are applicable to the entire Program -shall be treated as though they were included in this License, to the extent that they are valid -under applicable law. If additional permissions apply only to part of the Program, that part may be -used separately under those permissions, but the entire Program remains governed by this License -without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions -from that copy, or from any part of it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place additional permissions on -material, added by you to a covered work, for which you have or can give appropriate copyright -permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may -(if authorized by the copyright holders of that material) supplement the terms of this License with -terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - -All other non-permissive additional terms are considered "further restrictions" within the meaning -of section 10. If the Program as you received it, or any part of it, contains a notice stating that -it is governed by this License along with a term that is a further restriction, you may remove that -term. If a license document contains a further restriction but permits relicensing or conveying -under this License, you may add to a covered work material governed by the terms of that license -document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant -source files, a statement of the additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written -license, or stated as exceptions; the above requirements apply either way. - -8. Termination. - -You may not propagate or modify a covered work except as expressly provided under this License. Any -attempt otherwise to propagate or modify it is void, and will automatically terminate your rights -under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright -holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder fails to notify you of the -violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright -holder notifies you of the violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that copyright holder, and you cure -the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have -received copies or rights from you under this License. If your rights have been terminated and not -permanently reinstated, you do not qualify to receive new licenses for the same material under -section 10. - -9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run a copy of the Program. -Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer -transmission to receive a copy likewise does not require acceptance. However, nothing other than -this License grants you permission to propagate or modify any covered work. These actions infringe -copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, -you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically receives a license from the -original licensors, to run, modify and propagate that work, subject to this License. You are not -responsible for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an organization, or substantially -all assets of one, or subdividing an organization, or merging organizations. If propagation of a -covered work results from an entity transaction, each party to that transaction who receives a copy -of the work also receives whatever licenses to the work the party's predecessor in interest had or -could give under the previous paragraph, plus a right to possession of the Corresponding Source of -the work from the predecessor in interest, if the predecessor has it or can get it with reasonable -efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under -this License. For example, you may not impose a license fee, royalty, or other charge for exercise -of rights granted under this License, and you may not initiate litigation (including a cross-claim -or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, -offering for sale, or importing the Program or any portion of it. - -11. Patents. - -A "contributor" is a copyright holder who authorizes use under this License of the Program or a work -on which the Program is based. The work thus licensed is called the contributor's "contributor -version". - -A contributor's "essential patent claims" are all patent claims owned or controlled by the -contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, -permitted by this License, of making, using, or selling its contributor version, but do not include -claims that would be infringed only as a consequence of further modification of the contributor -version. For purposes of this definition, "control" includes the right to grant patent sublicenses -in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the -contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, -modify and propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement or commitment, -however denominated, not to enforce a patent (such as an express permission to practice a patent or -covenant not to sue for patent infringement). To "grant" such a patent license to a party means to -make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of -the work is not available for anyone to copy, free of charge and under the terms of this License, -through a publicly available network server or other readily accessible means, then you must either -(1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the -benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with -the requirements of this License, to extend the patent license to downstream recipients. "Knowingly -relying" means you have actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work in a country, would infringe -one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate -by procuring conveyance of, a covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of -the covered work, then the patent license you grant is automatically extended to all recipients of -the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of its coverage, -prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that -are specifically granted under this License. You may not convey a covered work if you are a party to -an arrangement with a third party that is in the business of distributing software, under which you -make payment to the third party based on the extent of your activity of conveying the work, and -under which the third party grants, to any of the parties who would receive the covered work from -you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in connection with specific -products or compilations that contain the covered work, unless you entered into that arrangement, or -that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other -defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict -the conditions of this License, they do not excuse you from the conditions of this License. If you -cannot convey a covered work so as to satisfy simultaneously your obligations under this License and -any other pertinent obligations, then as a consequence you may not convey it at all. For example, if -you agree to terms that obligate you to collect a royalty for further conveying from those to whom -you convey the Program, the only way you could satisfy both those terms and this License would be to -refrain entirely from conveying the Program. - -13. Remote Network Interaction; Use with the GNU General Public License. - -Notwithstanding any other provision of this License, if you modify the Program, your modified -version must prominently offer all users interacting with it remotely through a computer network (if -your version supports such interaction) an opportunity to receive the Corresponding Source of your -version by providing access to the Corresponding Source from a network server at no charge, through -some standard or customary means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 of the GNU General Public -License that is incorporated pursuant to the following paragraph. - -Notwithstanding any other provision of this License, you have permission to link or combine any -covered work with a work licensed under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this License will continue to apply to -the part which is the covered work, but the work with which it is combined will remain governed by -version 3 of the GNU General Public License. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of the GNU Affero General -Public License from time to time. Such new versions will be similar in spirit to the present -version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain -numbered version of the GNU Affero General Public License "or any later version" applies to it, you -have the option of following the terms and conditions either of that numbered version or of any -later version published by the Free Software Foundation. If the Program does not specify a version -number of the GNU Affero General Public License, you may choose any version ever published by the -Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU Affero General -Public License can be used, that proxy's public statement of acceptance of a version permanently -authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional -obligations are imposed on any author or copyright holder as a result of your choosing to follow a -later version. - -15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" -WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO -THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU -ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR -ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR -DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE -OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED -INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH -ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - -17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal -effect according to their terms, reviewing courts shall apply local law that most closely -approximates an absolute waiver of all civil liability in connection with the Program, unless a -warranty or assumption of liability accompanies a copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS diff --git a/lib/backend-server-data/readme.md b/lib/backend-server-data/readme.md deleted file mode 100644 index 133da3c32..000000000 --- a/lib/backend-server-data/readme.md +++ /dev/null @@ -1 +0,0 @@ -# Backend Data Server diff --git a/lib/backend-server-data/src/backend-server-data.impl.ts b/lib/backend-server-data/src/backend-server-data.impl.ts deleted file mode 100644 index d758ba7e2..000000000 --- a/lib/backend-server-data/src/backend-server-data.impl.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import type { Readable } from "stream" - -import { ConsoleLogger, TLogger } from "@ordo-pink/logger" -import type { TDataCommands } from "@ordo-pink/managers" -import { create_koa_server } from "@ordo-pink/backend-utils" - -import { handleCreateData } from "./handlers/create-data.handler" -import { handleGetAllData } from "./handlers/get-all-data.handler" -import { handleGetContent } from "./handlers/get-content.handler" -import { handleRemoveData } from "./handlers/remove-data.handler" -import { handleSetContent } from "./handlers/set-content.handler" -import { handleUpdateData } from "./handlers/update-data.handler" -import { handleUploadContent } from "./handlers/upload-content.handler" - -export type Params = { - origin: string | string[] - dataService: TDataCommands - idHost: string - logger?: TLogger -} - -export const createDataServer = ({ origin, dataService, idHost, logger = ConsoleLogger }: Params) => { - return create_koa_server({ - origin, - logger, - server_name: "dt", - extend_router: router => - router - .get("/", handleGetAllData({ dataService, idHost })) - .post("/:userId", handleCreateData({ dataService, idHost })) - .delete("/:userId/:fsid", handleRemoveData({ dataService, idHost })) - .get("/:userId/:fsid", handleGetContent({ dataService, idHost })) - .put("/:userId/:fsid", handleUpdateData({ dataService, idHost })) - .put("/:userId/:name/upload", handleUploadContent({ dataService, idHost })) - .put("/:userId/:fsid/update", handleSetContent({ dataService, idHost })), - }) -} diff --git a/lib/backend-server-data/src/handlers/create-data.handler.ts b/lib/backend-server-data/src/handlers/create-data.handler.ts deleted file mode 100644 index 2ec77b6e0..000000000 --- a/lib/backend-server-data/src/handlers/create-data.handler.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// // SPDX-FileCopyrightText: Copyright 2024, 谢尔盖||↓ and the Ordo.pink contributors -// // SPDX-License-Identifier: AGPL-3.0-only - -// // Ordo.pink is an all-in-one team workspace. -// // Copyright (C) 2024 谢尔盖||↓ and the Ordo.pink contributors - -// // This program is free software: you can redistribute it and/or modify -// // it under the terms of the GNU Affero General Public License as published -// // by the Free Software Foundation, either version 3 of the License, or -// // (at your option) any later version. - -// // This program is distributed in the hope that it will be useful, -// // but WITHOUT ANY WARRANTY; without even the implied warranty of -// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// // GNU Affero General Public License for more details. - -// // You should have received a copy of the GNU Affero General Public License -// // along with this program. If not, see . - -// import { type Middleware } from "koa" -// import { type Readable } from "stream" - -// import { type FSID, type TDataCommands } from "@ordo-pink/managers" -// import { authenticate0, parse_body0, send_error } from "@ordo-pink/backend-utils" -// import { HttpError } from "@ordo-pink/rrr" -// import { Oath } from "@ordo-pink/oath" - -// type Params = { dataService: TDataCommands; idHost: string } - -// export const handleCreateData = -// ({ dataService, idHost }: Params): Middleware => -// ctx => -// authenticate0(ctx, idHost) -// .chain(({ payload }) => -// dataService.dataPersistenceStrategy -// .count(payload.sub) -// .rejectedMap(() => HttpError.NotFound("User not found")) -// .chain( -// Oath.ifElse(totalFiles => totalFiles < payload.lim, { -// onFalse: () => HttpError.PaymentRequired("Subscription file limit exceeded"), -// }), -// ) -// .map(() => payload), -// ) -// .chain(({ sub, lim }) => -// parse_body0<{ name: string; parent: FSID | null; fsid?: FSID; labels?: string[] }>( -// ctx, -// ).chain(({ name, parent, fsid, labels }) => -// Oath.of(ctx.params.userId).chain(() => -// dataService -// .create({ createdBy: sub, name, parent, fsid, fileLimit: lim, labels }) -// .rejectedMap(HttpError.Conflict), -// ), -// ), -// ) -// .fork(send_error(ctx), result => { -// ctx.response.status = 201 -// ctx.response.body = { success: true, result } -// }) diff --git a/lib/backend-server-data/src/handlers/get-all-data.handler.ts b/lib/backend-server-data/src/handlers/get-all-data.handler.ts deleted file mode 100644 index f1d473daa..000000000 --- a/lib/backend-server-data/src/handlers/get-all-data.handler.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { authenticate0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { type TDataCommands } from "@ordo-pink/managers" - -type Params = { dataService: TDataCommands; idHost: string } - -export const handleGetAllData = - ({ dataService, idHost }: Params): Middleware => - ctx => - authenticate0(ctx, idHost) - .map(({ payload }) => payload) - .chain(({ sub }) => dataService.fetch({ createdBy: sub }).rejectedMap(HttpError.NotFound)) - .fork(send_error(ctx), result => { - ctx.response.status = 200 - ctx.response.body = { success: true, result } - }) diff --git a/lib/backend-server-data/src/handlers/get-content.handler.ts b/lib/backend-server-data/src/handlers/get-content.handler.ts deleted file mode 100644 index e734ebe7f..000000000 --- a/lib/backend-server-data/src/handlers/get-content.handler.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { type FSID, type TDataCommands } from "@ordo-pink/managers" -import { authenticate0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { Oath } from "@ordo-pink/oath" -import { type SUB } from "@ordo-pink/wjwt" -import { type Unary } from "@ordo-pink/tau" - -export const handleGetContent: Unary<{ dataService: TDataCommands; idHost: string }, Middleware> = - ({ dataService, idHost }) => - ctx => - authenticate0(ctx, idHost) - .chain(() => - Oath.of({ fsid: ctx.params.fsid as FSID, createdBy: ctx.params.userId as SUB }).chain(({ fsid, createdBy }) => - dataService.getContent({ fsid, createdBy }).rejectedMap(HttpError.NotFound), - ), - ) - .fork( - send_error(ctx), - async result => - new Promise(resolve => { - ctx.response.status = 200 - result.on("data", chunk => { - ctx.res.write(chunk) - }) - result.on("error", () => { - ctx.res.write("") - ctx.res.end() - }) - result.on("end", () => { - resolve(ctx.res.end()) - }) - }), - ) diff --git a/lib/backend-server-data/src/handlers/remove-data.handler.ts b/lib/backend-server-data/src/handlers/remove-data.handler.ts deleted file mode 100644 index 4e3a9b893..000000000 --- a/lib/backend-server-data/src/handlers/remove-data.handler.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { type FSID, type TDataCommands } from "@ordo-pink/managers" -import { authenticate0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { Oath } from "@ordo-pink/oath" -import { type SUB } from "@ordo-pink/wjwt" -import { type Unary } from "@ordo-pink/tau" - -export const handleRemoveData: Unary<{ dataService: TDataCommands; idHost: string }, Middleware> = - ({ dataService, idHost }) => - ctx => - authenticate0(ctx, idHost) - .chain(() => - Oath.of({ fsid: ctx.params.fsid as FSID, createdBy: ctx.params.userId as SUB }).chain(({ fsid, createdBy }) => - dataService.remove({ fsid, createdBy }).rejectedMap(HttpError.NotFound), - ), - ) - .fork(send_error(ctx), result => { - ctx.response.body = { success: true, result } - }) diff --git a/lib/backend-server-data/src/handlers/set-content.handler.ts b/lib/backend-server-data/src/handlers/set-content.handler.ts deleted file mode 100644 index 1ecc66b38..000000000 --- a/lib/backend-server-data/src/handlers/set-content.handler.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { type FSID, type TDataCommands } from "@ordo-pink/managers" -import { authenticate0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { Oath } from "@ordo-pink/oath" -import { type SUB } from "@ordo-pink/wjwt" -import { type Unary } from "@ordo-pink/tau" - -export const handleSetContent: Unary<{ dataService: TDataCommands; idHost: string }, Middleware> = - ({ dataService, idHost }) => - ctx => - authenticate0(ctx, idHost) - .chain(({ payload }) => - Oath.FromNullable(ctx.req.headers["content-length"]).bimap( - () => HttpError.UnprocessableEntity("Unknown size"), - () => payload, - ), - ) - .chain( - Oath.ifElse(payload => Number(ctx.req.headers["content-length"]) / 1024 / 1024 <= payload.fms, { - onFalse: () => HttpError.PayloadTooLarge("File too large"), - }), - ) - .chain(({ sub }) => - Oath.of({ fsid: ctx.params.fsid as FSID, createdBy: ctx.params.userId as SUB }).chain(({ fsid, createdBy }) => - dataService - .updateContent({ - fsid, - createdBy, - updatedBy: sub, - content: ctx.request.req, - length: Number(ctx.req.headers["content-length"]), - }) - .rejectedMap(HttpError.NotFound), - ), - ) - .fork(send_error(ctx), result => { - ctx.response.status = 200 - ctx.response.body = { success: true, result } - }) diff --git a/lib/backend-server-data/src/handlers/update-data.handler.ts b/lib/backend-server-data/src/handlers/update-data.handler.ts deleted file mode 100644 index ea3deb70a..000000000 --- a/lib/backend-server-data/src/handlers/update-data.handler.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { type FSID, type PlainData, type TDataCommands } from "@ordo-pink/managers" -import { authenticate0, parse_body0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { Oath } from "@ordo-pink/oath" -import { type SUB } from "@ordo-pink/wjwt" -import { type Unary } from "@ordo-pink/tau" - -export const handleUpdateData: Unary<{ dataService: TDataCommands; idHost: string }, Middleware> = - ({ dataService, idHost }) => - ctx => - authenticate0(ctx, idHost) - .map(({ payload }) => payload) - .chain(({ sub }) => - parse_body0(ctx).chain(data => - Oath.of({ fsid: ctx.params.fsid as FSID, createdBy: ctx.params.userId as SUB }).chain(({ fsid, createdBy }) => - dataService.update({ fsid, createdBy, updatedBy: sub, data }).rejectedMap(HttpError.NotFound), - ), - ), - ) - .fork(send_error(ctx), result => { - ctx.response.status = 200 - ctx.response.body = { success: true, result } - }) diff --git a/lib/backend-server-data/src/handlers/upload-content.handler.ts b/lib/backend-server-data/src/handlers/upload-content.handler.ts deleted file mode 100644 index cb4e4d6bf..000000000 --- a/lib/backend-server-data/src/handlers/upload-content.handler.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { type Middleware } from "koa" -import { type Readable } from "stream" - -import { type FSID, type TDataCommands } from "@ordo-pink/managers" -import { authenticate0, send_error } from "@ordo-pink/backend-utils" -import { HttpError } from "@ordo-pink/rrr" -import { Oath } from "@ordo-pink/oath" -import { type SUB } from "@ordo-pink/wjwt" -import { type Unary } from "@ordo-pink/tau" - -export const handleUploadContent: Unary<{ dataService: TDataCommands; idHost: string }, Middleware> = - ({ dataService, idHost }) => - ctx => - authenticate0(ctx, idHost) - .chain(({ payload }) => - Oath.FromNullable(ctx.req.headers["content-length"]).bimap( - () => HttpError.UnprocessableEntity("Unknown size"), - () => payload, - ), - ) - .chain( - Oath.ifElse(payload => Number(ctx.req.headers["content-length"]) / 1024 / 1024 <= payload.fms, { - onFalse: () => HttpError.PayloadTooLarge("File too large"), - }), - ) - .chain(({ sub, lim }) => - Oath.of({ name: ctx.params.name as string, createdBy: ctx.params.userId as SUB }).chain(({ name, createdBy }) => - Oath.of((ctx.get("ordo-parent") as FSID) || null).chain(parent => - dataService - .uploadContent({ - name, - parent, - createdBy, - updatedBy: sub, - content: ctx.request.req, - fileLimit: lim, - length: Number(ctx.req.headers["content-length"]), - }) - .rejectedMap(HttpError.NotFound), - ), - ), - ) - .fork(send_error(ctx), () => { - ctx.response.status = 200 - ctx.response.body = { success: true, result: ctx.req.headers["content-length"] } - }) diff --git a/lib/backend-util-create-response/src/backend-util-create-response.impl.ts b/lib/backend-util-create-response/src/backend-util-create-response.impl.ts index 75eb09ac6..025a7e049 100644 --- a/lib/backend-util-create-response/src/backend-util-create-response.impl.ts +++ b/lib/backend-util-create-response/src/backend-util-create-response.impl.ts @@ -5,20 +5,36 @@ import { RRR } from "@ordo-pink/core" import { Switch } from "@ordo-pink/switch" +import { type TDefaultContext } from "@ordo-pink/backend-util-default-handler" import { type TIntake } from "@ordo-pink/routary" -import { TSharedContext } from "@ordo-pink/backend-id" -export const create_response = < +export const create_json_response = < $TResult, - $TIntake extends TIntake<{ payload?: $TResult; headers: Record; status: number }>, + $TIntake extends TIntake<{ payload?: $TResult; headers: Headers; status: number }>, >({ headers, payload, status, -}: $TIntake): Response => new Response(JSON.stringify({ success: status <= 399, payload }), { headers, status }) +}: $TIntake): Response => Response.json({ success: status <= 399, payload }, { headers, status }) + +export const create_response = <$TResult, $TIntake extends TIntake<{ payload?: $TResult; headers: Headers; status: number }>>({ + headers, + payload, + status, +}: $TIntake): Response => new Response(payload as any, { headers, status }) + +type TStatusFromRRRParams<$TContext extends TDefaultContext> = { rrr: Ordo.Rrr; intake: TIntake<$TContext> } +export const status_from_rrr = <$TContext extends TDefaultContext>({ + rrr, + intake, +}: TStatusFromRRRParams<$TContext>): TIntake<$TContext> => { + if (intake.headers.get("Content-Type") !== "application/json") { + intake.headers.set("Content-Type", "application/json") + intake.payload = JSON.stringify({ success: false, payload: rrr.message }) + } else { + intake.payload = rrr.message + } -type TStatusFromRRRParams = { rrr: Ordo.Rrr; intake: TIntake } -export const status_from_rrr = ({ rrr, intake }: TStatusFromRRRParams): TIntake => { intake.status = Switch.Match(rrr.code) .case([RRR.enum.EAGAIN, RRR.enum.ENXIO], () => 408) .case([RRR.enum.EFBIG, RRR.enum.ENOSPC], () => 413) @@ -29,7 +45,5 @@ export const status_from_rrr = ({ rrr, intake }: TStatusFromRRRParams): TIntake< .case(RRR.enum.EEXIST, () => 409) .default(() => 500) - intake.payload = rrr.message - return intake } diff --git a/lib/backend-util-default-handler/index.ts b/lib/backend-util-default-handler/index.ts new file mode 100644 index 000000000..af3df5c8f --- /dev/null +++ b/lib/backend-util-default-handler/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/backend-util-default-handler.impl" +export * from "./src/backend-util-default-handler.types" diff --git a/lib/backend-util-default-handler/license b/lib/backend-util-default-handler/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/backend-util-default-handler/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/backend-util-default-handler/readme.md b/lib/backend-util-default-handler/readme.md new file mode 100644 index 000000000..4566ca378 --- /dev/null +++ b/lib/backend-util-default-handler/readme.md @@ -0,0 +1 @@ +# Backend Util Default Handler diff --git a/lib/backend-id/src/handlers/default.handler.ts b/lib/backend-util-default-handler/src/backend-util-default-handler.impl.ts similarity index 57% rename from lib/backend-id/src/handlers/default.handler.ts rename to lib/backend-util-default-handler/src/backend-util-default-handler.impl.ts index abaf1d79a..3abaa07ab 100644 --- a/lib/backend-id/src/handlers/default.handler.ts +++ b/lib/backend-util-default-handler/src/backend-util-default-handler.impl.ts @@ -1,21 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + import { Oath, invokers0, ops0 } from "@ordo-pink/oath" import { type TGear, type TIntake } from "@ordo-pink/routary" -import { create_response, status_from_rrr } from "@ordo-pink/backend-util-create-response" +import { create_json_response, status_from_rrr } from "@ordo-pink/backend-util-create-response" import { set_x_response_time_header, start_response_timer, stop_response_timer } from "@ordo-pink/backend-util-response-time" +import { extract_request_ip } from "@ordo-pink/backend-util-extract-request-ip" import { log_request } from "@ordo-pink/backend-util-log-request" import { set_content_type_application_json_header } from "@ordo-pink/backend-util-set-header" -import { type TIDChamber, type TSharedContext } from "../backend-id.types" -import { extract_request_ip } from "@ordo-pink/backend-util-extract-request-ip" +import { TDefaultContext } from "./backend-util-default-handler.types" export const default_handler = - ( - custom_handler: ( - intake: TIntake, - ) => Oath, { rrr: Ordo.Rrr; intake: TIntake }>, - ): TGear => + <$TContext extends TDefaultContext>( + custom_handler: (intake: TIntake<$TContext>) => Oath, { rrr: Ordo.Rrr; intake: TIntake<$TContext> }>, + ): TGear<$TContext> => intake => - Oath.Resolve>({ ...intake, status: 200, request_ip: null }) + Oath.Resolve>({ ...intake, status: 200, request_ip: null, headers: intake.headers ?? new Headers() }) .pipe(ops0.tap(start_response_timer)) .pipe(ops0.tap(extract_request_ip)) .pipe(ops0.tap(set_content_type_application_json_header)) @@ -24,5 +27,5 @@ export const default_handler = .pipe(ops0.tap(stop_response_timer)) .pipe(ops0.tap(set_x_response_time_header)) .pipe(ops0.tap(log_request)) - .pipe(ops0.map(create_response)) + .pipe(ops0.map(create_json_response)) .invoke(invokers0.force_resolve) diff --git a/lib/backend-util-default-handler/src/backend-util-default-handler.test.ts b/lib/backend-util-default-handler/src/backend-util-default-handler.test.ts new file mode 100644 index 000000000..eb495b8ef --- /dev/null +++ b/lib/backend-util-default-handler/src/backend-util-default-handler.test.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { expect, test } from "bun:test" +import { backend_util_default_handler } from "./backend-util-default-handler.impl" + +test("backend-util-default-handler should pass", () => { + expect(backend_util_default_handler).toEqual("backend-util-default-handler") +}) diff --git a/lib/backend-util-default-handler/src/backend-util-default-handler.types.ts b/lib/backend-util-default-handler/src/backend-util-default-handler.types.ts new file mode 100644 index 000000000..f96ac9660 --- /dev/null +++ b/lib/backend-util-default-handler/src/backend-util-default-handler.types.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import type { SocketAddress } from "bun" + +import type { TLogger } from "@ordo-pink/logger" + +export type TDefaultContext = { + headers: Headers + logger: TLogger + payload?: unknown + request_ip: SocketAddress | null + status: number + response_time?: string + response_timer?: number +} diff --git a/lib/backend-util-log-request/src/backend-util-log-request.impl.ts b/lib/backend-util-log-request/src/backend-util-log-request.impl.ts index fd37eb5a7..9c561ad15 100644 --- a/lib/backend-util-log-request/src/backend-util-log-request.impl.ts +++ b/lib/backend-util-log-request/src/backend-util-log-request.impl.ts @@ -24,7 +24,7 @@ export const log_request = < const pathname = url_obj.pathname.endsWith("/") && url_obj.pathname.length > 1 ? url_obj.pathname.slice(0, -1) : url_obj.pathname - const request_address = request_ip ? `${request_ip?.address}:${request_ip?.port}` : "localhost" + const request_address = request_ip ? `${request_ip?.address}:${request_ip?.port}` : "" const url = `${pathname}${url_obj.search}` logger.info(`${request_address} :: ${status} ${method} ${url} (${response_time}ms)`) diff --git a/lib/backend-util-response-time/src/backend-util-response-time.impl.ts b/lib/backend-util-response-time/src/backend-util-response-time.impl.ts index 25f583074..6661336a1 100644 --- a/lib/backend-util-response-time/src/backend-util-response-time.impl.ts +++ b/lib/backend-util-response-time/src/backend-util-response-time.impl.ts @@ -3,21 +3,19 @@ * SPDX-License-Identifier: Unlicense */ -import { hrtime } from "node:process" +import { TDefaultContext } from "@ordo-pink/backend-util-default-handler" +import { TIntake } from "@ordo-pink/routary" -import { type TBackendUtilResponseTimeExpectedIntake } from "./backend-util-response-time.types" - -export const start_response_timer = <$TIntake extends TBackendUtilResponseTimeExpectedIntake>(intake: $TIntake): void => { - intake.response_timer = hrtime() +export const start_response_timer = <$TIntake extends TIntake>(intake: $TIntake): void => { + intake.response_timer = Date.now() } -export const stop_response_timer = <$TIntake extends TBackendUtilResponseTimeExpectedIntake>(intake: $TIntake): void => { - const end_time = hrtime(intake.response_timer) - const response_time = end_time[0] * 1e3 + end_time[1] / 1e6 +export const stop_response_timer = <$TIntake extends TIntake>(intake: $TIntake): void => { + const end_time = Date.now() - intake.response_timer! - intake.response_time = response_time.toFixed(3) + intake.response_time = end_time.toString() } -export const set_x_response_time_header = <$TIntake extends TBackendUtilResponseTimeExpectedIntake>(intake: $TIntake): void => { - if (intake.response_time) intake.headers["X-Response-Time"] = intake.response_time.toString() +export const set_x_response_time_header = <$TIntake extends TIntake>(intake: $TIntake): void => { + if (intake.response_time) intake.headers.set("X-Response-Time", intake.response_time) } diff --git a/lib/backend-util-response-time/src/backend-util-response-time.types.ts b/lib/backend-util-response-time/src/backend-util-response-time.types.ts index 719352230..ba8094dc7 100644 --- a/lib/backend-util-response-time/src/backend-util-response-time.types.ts +++ b/lib/backend-util-response-time/src/backend-util-response-time.types.ts @@ -5,6 +5,6 @@ export type TBackendUtilResponseTimeExpectedIntake = { response_time?: string - response_timer?: [number, number] - headers: Record + response_timer?: number + headers: Headers } diff --git a/lib/backend-util-set-header/src/backend-util-set-header.impl.ts b/lib/backend-util-set-header/src/backend-util-set-header.impl.ts index 34654087b..5077a46cf 100644 --- a/lib/backend-util-set-header/src/backend-util-set-header.impl.ts +++ b/lib/backend-util-set-header/src/backend-util-set-header.impl.ts @@ -5,8 +5,8 @@ export const set_header = (key: string, value: string) => - <$TIntake extends { headers: Record }>(intake: $TIntake) => { - intake.headers[key] = value + <$TIntake extends { headers: Headers }>(intake: $TIntake) => { + intake.headers.set(key, value) } export const set_content_type_application_json_header = set_header("Content-Type", "application/json") diff --git a/lib/backend/src/backend.types.ts b/lib/backend/src/backend.types.ts index 8b08da7bd..7047b2c71 100644 --- a/lib/backend/src/backend.types.ts +++ b/lib/backend/src/backend.types.ts @@ -25,6 +25,25 @@ import type { TValidations } from "@ordo-pink/core" declare global { module OrdoBackend { + module Data { + type PersistenceStrategy = { + exists: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + create: ( + uid: Ordo.User.UID, + fsid: Ordo.Metadata.FSID, + input: ReadableStream, + ) => Oath> + read: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + update: ( + uid: Ordo.User.UID, + fsid: Ordo.Metadata.FSID, + input: ReadableStream, + ) => Oath> + delete: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + mtime: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + } + } + module Notification { type EmailStrategy = { send: (params: { @@ -46,15 +65,15 @@ declare global { } type PersistenceStrategy = { - exists_by_id: (id: Ordo.User.ID) => Oath> + exists_by_id: (id: Ordo.User.UID) => Oath> exists_by_email: (email: Ordo.User.Email) => Oath> exists_by_handle: (handle: Ordo.User.Handle) => Oath> - get_by_id: (id: Ordo.User.ID) => Oath> + get_by_id: (id: Ordo.User.UID) => Oath> get_by_email: (email: Ordo.User.Email) => Oath> get_by_handle: (handle: Ordo.User.Handle) => Oath> create: (user: DTO) => Oath> - update: (id: Ordo.User.ID, user: DTO) => Oath> - remove: (id: Ordo.User.ID) => Oath> + update: (id: Ordo.User.UID, user: DTO) => Oath> + remove: (id: Ordo.User.UID) => Oath> } export type Validations = TValidations diff --git a/lib/core/src/constants.ts b/lib/core/src/constants.ts index f059f6c56..65f8af7ed 100644 --- a/lib/core/src/constants.ts +++ b/lib/core/src/constants.ts @@ -22,6 +22,7 @@ export const OK = "OK" as const export const LIB_DIRECTORY_FSID = "1de21bf3-2277-4d3a-bbd3-d120eb8a49d0" +export const METADATA_CONTENT_FSID = "b68678af-6776-47c0-a0b8-4b6535664b8c" export const ACTIVITY_BAR_WIDTH = 48 export const SIDEBAR_WORKSPACE_GUTTER_WIDTH = 10 diff --git a/lib/core/src/metadata-validations.impl.ts b/lib/core/src/metadata-validations.impl.ts index 0aa0e08b0..81e40f5f7 100644 --- a/lib/core/src/metadata-validations.impl.ts +++ b/lib/core/src/metadata-validations.impl.ts @@ -38,13 +38,13 @@ export const is_size = (size: unknown): size is number => TAU.is_finite_non_nega export const is_type = (type: unknown): type is string => TAU.is_non_empty_string(type) // TODO: MIME-TYPE -export const is_created_by = (author: unknown): author is Ordo.User.ID => TAU.is_uuid(author) +export const is_created_by = (author: unknown): author is Ordo.User.UID => TAU.is_uuid(author) -export const is_updated_by = (author: unknown): author is Ordo.User.ID => TAU.is_uuid(author) +export const is_updated_by = (author: unknown): author is Ordo.User.UID => TAU.is_uuid(author) -export const is_created_at = (timestamp: unknown): timestamp is Date => TAU.is_finite_non_negative_int(timestamp) +export const is_created_at = (timestamp: unknown): timestamp is number => TAU.is_finite_non_negative_int(timestamp) -export const is_updated_at = (timestamp: unknown): timestamp is Date => TAU.is_finite_non_negative_int(timestamp) +export const is_updated_at = (timestamp: unknown): timestamp is number => TAU.is_finite_non_negative_int(timestamp) export const is_props = <$TProps extends Ordo.Metadata.Props>(props?: $TProps): props is $TProps => props === undefined || TAU.keys_of(props).reduce((acc, key) => acc && is_prop_key(key), true) @@ -59,6 +59,10 @@ export const is_label = (label: unknown): label is TAU.Unpack TAU.is_bool(x) || TAU.is_undefined(x) + +export const is_checksum = (x: unknown): x is string | undefined => TAU.is_non_empty_string(x) || TAU.is_undefined(x) + export const is_link = (link: unknown): link is TAU.Unpack => TAU.is_uuid(link) export const is_metadata = (x: unknown): x is Ordo.Metadata.Instance => { @@ -107,6 +111,8 @@ export const is_metadata_dto = (x: unknown): x is Ordo.Metadata.DTO => { is_props(y.props) && is_size(y.size) && is_type(y.type) && + is_is_deleted(y.is_deleted) && + is_checksum(y.checksum) && is_created_at(y.updated_at) && is_created_by(y.updated_by) ) @@ -130,4 +136,8 @@ export const MetadataValidations: Ordo.Metadata.Validations = { is_type, are_labels, are_links, + is_is_deleted, + is_checksum, + is_labels: are_labels, + is_links: are_links, } diff --git a/lib/core/src/metadata.impl.ts b/lib/core/src/metadata.impl.ts index 85c36594b..cf4ed3f53 100644 --- a/lib/core/src/metadata.impl.ts +++ b/lib/core/src/metadata.impl.ts @@ -77,6 +77,8 @@ export const Metadata: Ordo.Metadata.Static = { ) }, is_item_of: o => Metadata.Validations.is_metadata_dto(o) && Metadata.FromDTO(o).equals(Metadata.FromDTO(dto)), + validate: checksum => Metadata.Validations.is_checksum(checksum) && dto.checksum === checksum, + is_deleted: () => !!dto.is_deleted, }), Validations: MetadataValidations, } diff --git a/lib/core/src/types.ts b/lib/core/src/types.ts index bc88082b3..d76a172b9 100644 --- a/lib/core/src/types.ts +++ b/lib/core/src/types.ts @@ -19,7 +19,6 @@ * along with this program. If not, see . */ -import type { JTI, SUB } from "@ordo-pink/wjwt" import type { TMaokaChildren, TMaokaComponent } from "@ordo-pink/maoka" import type { Oath } from "@ordo-pink/oath" import type { TLogger } from "@ordo-pink/logger" @@ -303,6 +302,7 @@ declare global { remove_links: () => { fsid: Ordo.Metadata.FSID; links: Ordo.Metadata.FSID[] } set_property: () => { fsid: Ordo.Metadata.FSID; key: string; value: any } set_size: () => { fsid: Ordo.Metadata.FSID; size: number } + publish: () => { ctx: Ordo.CreateFunction.State; fsid: Ordo.Metadata.FSID; name?: string; styles?: string[] } } content: { set: () => { fsid: Ordo.Metadata.FSID; content: Ordo.Content.Instance; content_type: string } @@ -351,7 +351,7 @@ declare global { type Fetch = typeof window.fetch - type Hosts = { id: string; dt: string; static: string; website: string } + type Hosts = { id: string; dt: string; pb: string; web: string } /** * User achievements and whatever else related to using them. @@ -563,15 +563,15 @@ declare global { type RenderParams = { is_editable: boolean is_embedded: boolean - content: Ordo.Content.Instance | null + content: Ordo.Content.Instance metadata: Ordo.Metadata.Instance } } namespace User { type Handle = `@${string}` // TODO Disallow forbidden chars - type ID = `${string}-${string}-${string}-${string}-${string}` // TODO Strict type - type Email = `${string}@${string}.${string}` // TODO Strict type + type UID = `${string}-${string}-${string}-${string}-${string}` + type Email = `${string}@${string}.${string}` namespace Current { type DTO = Ordo.User.Public.DTO & { @@ -603,30 +603,11 @@ declare global { Serialize: <$TDTO extends Ordo.User.Current.DTO>(dto: $TDTO) => Ordo.User.Current.DTO Validations: Ordo.User.Current.Validations } - - type Repository = { - get: () => TResult> - put: (user: Ordo.User.Current.Instance) => TResult> - get $(): TZags<{ version: number }> - } - - type RepositoryStatic = { - Of: ($: TZags<{ user: Ordo.User.Current.Instance | null }>) => Ordo.User.Current.Repository - } - - type RepositoryAsync = { - get: (token: string) => Oath> - put: (token: string, user: Ordo.User.Current.Instance) => Oath> - } - - type RepositoryAsyncStatic = { - Of: (id_host: string, fetch: Ordo.Fetch) => RepositoryAsync - } } namespace Public { type DTO = { - id: Ordo.User.ID + id: Ordo.User.UID created_at: number subscription: C.UserSubscription handle: Ordo.User.Handle @@ -645,7 +626,7 @@ declare global { } type Instance = { - get_id: () => Ordo.User.ID + get_id: () => Ordo.User.UID get_created_at: () => Date get_subscription: () => C.UserSubscription get_handle: () => Handle @@ -658,75 +639,56 @@ declare global { is_paid: () => boolean to_dto: () => Ordo.User.Public.DTO } - - type Repository = { - get: () => Oath> - put: (users: Ordo.User.Public.Instance[]) => Oath> - get $(): TZags<{ version: number }> - } - - type RepositoryStatic = { - Of: (known_user_zags: TZags<{ known_users: Ordo.User.Public.Instance[] }>) => Repository - } } type Query = { - get_current: () => TResult> - // get_current_by_id: ( - // id: Ordo.User.Current.Instance["id"], - // ) => Oath, Ordo.Rrr<"EPERM" | "EAGAIN" | "EINVAL" | "EIO">> - get_by_id: ( - email: Ordo.User.ID, - ) => Oath> - - // get_by_handle: ( - // handle: Ordo.User.Current.Instance["handle"], - // ) => Oath, Ordo.Rrr<"EPERM" | "EAGAIN" | "EINVAL" | "EIO">> + is_authenticated: () => boolean + get_current: () => TResult> + get_by_id: (uid: Ordo.User.UID) => Oath> + get_by_handle: (handle: Ordo.User.Handle) => Oath> get $(): TZags<{ version: number }> } type QueryStatic = { Of: ( - current_user_repository: Ordo.User.Current.Repository, - public_user_repository: Ordo.User.Public.Repository, - check_query_permission: (permission: Ordo.CreateFunction.QueryPermission) => TResult>, + check_permission: (permission: Ordo.CreateFunction.QueryPermission) => TResult>, ) => Ordo.User.Query } } + // TODO improve types namespace Content { - type Instance = string | ArrayBuffer | Blob | FormData | Uint8Array | Record - - type Storage = Record + type Instance = string | ArrayBuffer | Blob | FormData | Uint8Array | ReadableStream | null + + type PersistenceStrategy = { + clear: () => Oath> + delete: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + exists: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + get: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => Oath> + list: () => Oath, Ordo.Rrr<"EIO">> + put: (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID, content: Ordo.Content.Instance) => Oath> + } type RepositoryStatic = { - Of: (data_host: string, fetch: Ordo.Fetch) => Repository + Of: ( + auth$: TZags<{ user: Ordo.User.Current.Instance | null; token: string | null }>, + local_strategy: Ordo.Content.PersistenceStrategy, + remote_strategy: Ordo.Content.PersistenceStrategy, + ) => Repository } - type ContentType = "text" | "array_buffer" | "blob" | "form_data" | "json" | "bytes" | "unwrapped" - type Repository = { - get: <$TContentType extends Ordo.Content.ContentType>( + get: ( + uid: Ordo.User.UID, + fsid: Ordo.Metadata.FSID, + ) => Oath> + get_all: () => Oath, Ordo.Rrr<"EIO">> + put: ( + uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID, - content_type: $TContentType, - ) => Oath< - | ($TContentType extends "text" - ? string - : $TContentType extends "array_buffer" - ? ArrayBuffer - : $TContentType extends "blob" - ? Blob - : $TContentType extends "form_data" - ? FormData - : $TContentType extends "bytes" - ? Uint8Array - : $TContentType extends "unwrapped" - ? Response - : unknown) - | null, - Ordo.Rrr<"EIO" | "EACCES" | "EINVAL" | "ENOENT"> - > - put: (fsid: Ordo.Metadata.FSID, content: Ordo.Content.Instance) => Oath> + content: Ordo.Content.Instance, + ) => Oath> + get $(): TZags<{ version: number }> } type QueryStatic = { @@ -737,26 +699,10 @@ declare global { } type Query = { - get: <$TContentType extends Ordo.Content.ContentType>( + get: ( + uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID, - content_type: $TContentType, - ) => Oath< - | ($TContentType extends "text" - ? string - : $TContentType extends "array_buffer" - ? ArrayBuffer - : $TContentType extends "blob" - ? Blob - : $TContentType extends "form_data" - ? FormData - : $TContentType extends "bytes" - ? Uint8Array - : $TContentType extends "unwrapped" - ? Response - : unknown) - | null, - Ordo.Rrr<"EPERM" | "EIO" | "EACCES" | "EINVAL" | "ENOENT"> - > + ) => Oath> } } @@ -777,16 +723,18 @@ declare global { labels: Ordo.Metadata.Label[] type: string created_at: number - created_by: Ordo.User.ID + created_by: Ordo.User.UID updated_at: number - updated_by: Ordo.User.ID + updated_by: Ordo.User.UID size: number props?: $TProps + is_deleted?: boolean + checksum?: string }> type Static = { Of: <$TProps extends Ordo.Metadata.Props = Ordo.Metadata.Props>( - params: Ordo.Metadata.CreateParams<$TProps> & { author_id: Ordo.User.ID }, + params: Ordo.Metadata.CreateParams<$TProps> & { author_id: Ordo.User.UID }, ) => Ordo.Metadata.Instance<$TProps> FromDTO: <$TProps extends Ordo.Metadata.Props = Ordo.Metadata.Props>( dto: Ordo.Metadata.DTO<$TProps>, @@ -809,9 +757,9 @@ declare global { get_label_index: (label: Ordo.Metadata.Label) => number get_type: () => string get_created_at: () => Date - get_created_by: () => Ordo.User.ID + get_created_by: () => Ordo.User.UID get_updated_at: () => Date - get_updated_by: () => Ordo.User.ID + get_updated_by: () => Ordo.User.UID get_size: () => number get_readable_size: () => string get_property: <_TKey extends keyof $TProps>(key: _TKey) => NonNullable<$TProps[_TKey]> | null @@ -819,24 +767,16 @@ declare global { equals: (other_metadata?: Ordo.Metadata.Instance) => boolean is_item_of: (dto: Ordo.Metadata.DTO) => boolean is_hidden: () => boolean + validate: (checksum: string) => boolean + is_deleted: () => boolean } - type Validations = { + type Validations = TValidations & { is_metadata: (x: unknown) => x is Ordo.Metadata.Instance is_metadata_dto: (x: unknown) => x is Ordo.Metadata.DTO - is_created_at: (x: unknown) => x is Date - is_created_by: (x: unknown) => x is Ordo.User.ID - is_updated_at: (x: unknown) => x is Date - is_updated_by: (x: unknown) => x is Ordo.User.ID - is_fsid: (x: unknown) => x is Ordo.Metadata.FSID is_label: (x: unknown) => x is Ordo.Metadata.Label is_link: (x: unknown) => x is Ordo.Metadata.FSID - is_name: (x: unknown) => boolean - is_parent: (x: unknown) => boolean is_prop_key: (x: unknown) => boolean - is_props: (x: unknown) => boolean - is_size: (x: unknown) => boolean - is_type: (x: unknown) => boolean are_labels: (x: unknown) => boolean are_links: (x: unknown) => boolean } @@ -1296,333 +1236,5 @@ declare global { render_custom_info?: () => TMaokaComponent // TODO Use standard render approach } } - - namespace Routes { - type TSuccessResponse = { success: true; result: T } - type TErrorResponse = { success: false; error: string } - type TTokenResult = { - sub: SUB - jti: JTI - expires: Date - token: string - file_limit: Ordo.User.Current.DTO["file_limit"] - subscription: Ordo.User.Current.DTO["subscription"] - max_upload_size: Ordo.User.Current.DTO["max_upload_size"] - max_functions: Ordo.User.Current.DTO["max_functions"] - } - - namespace DT { - namespace SyncMetadata { - type Path = `/${Ordo.User.Current.DTO["id"]}` - type Method = "POST" - type Cookies = void - type Params = void - type RequestBody = Ordo.Metadata.DTO[] // TODO: Replace with array of atomic changes - type StatusCode = 200 - type ResponseBody = string - } - - namespace GetContent { - type Path = `/${Ordo.User.Current.DTO["id"]}/${Ordo.Metadata.FSID}` - type Method = "GET" - type Cookies = void - type Params = { user_id?: string; fsid?: string } - type RequestBody = void - type StatusCode = 200 - type ResponseBody = string | ArrayBuffer - } - - namespace SetContent { - type Path = `/${Ordo.User.Current.DTO["id"]}/${Ordo.Metadata.FSID}/${Ordo.Metadata.DTO["type"]}` - type Method = "PUT" - type Cookies = void - type Params = { user_id?: string; fsid?: string } - type RequestBody = string | ArrayBuffer - type StatusCode = 204 - type ResponseBody = void - } - - namespace CreateContent { - type Path = `/${Ordo.User.Current.DTO["id"]}` - type Url = - `${string}${Path}?name=${string}&parent=${Ordo.Metadata.DTO["parent"]}&content_type=${Ordo.Metadata.DTO["type"]}` - type Method = "POST" - type Cookies = void - type Params = { user_id?: string; name?: string; parent?: string } - type RequestBody = string | ArrayBuffer - type StatusCode = 201 - type ResponseBody = void - } - } - - namespace ID { - namespace UpdateInfo { - type Path = "/account/info" - type Method = "PATCH" - type Cookies = void - type Params = void - type RequestBody = { first_name?: string; last_name?: string } - type StatusCode = 200 - type ResponseBody = Ordo.User.Current.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace UpdateEmail { - type Path = "/account/email" - type Method = "PATCH" - type Cookies = void - type Params = void - type StatusCode = 200 - type RequestBody = { email?: string } - type ResponseBody = Ordo.User.Current.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace UpdateHandle { - type Path = "/account/handle" - type Method = "PATCH" - type Cookies = void - type Params = void - type StatusCode = 200 - type RequestBody = { handle?: string } - type ResponseBody = Ordo.User.Current.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace UpdatePassword { - type Path = "/account/password" - type Method = "PATCH" - type Cookies = void - type Params = void - type StatusCode = 200 - type RequestBody = { old_password?: string; new_password?: string } - type ResponseBody = TTokenResult - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace ConfirmEmail { - type Path = "/account/confirm-email" - type Url = `${string}${Path}?email=${string}&code=${string}` // TODO: Define host - type Method = "POST" - type Cookies = void - type Params = void - type StatusCode = 200 - type RequestBody = { email?: string; code?: string } - type ResponseBody = Ordo.User.Current.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace GetAccount { - type Path = "/account" - type Method = "GET" - type Cookies = void - type Params = void - type RequestBody = void - type StatusCode = 200 - type ResponseBody = Ordo.User.Current.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace GetUserByEmail { - type Path = `/users/email/${string}` - type Method = "GET" - type Cookies = void - type Params = { email?: string } - type RequestBody = void - type StatusCode = 200 - type ResponseBody = Ordo.User.Public.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace GetUserByHandle { - type Path = `/users/handle/${string}` - type Method = "GET" - type Cookies = void - type Params = { handle?: string } - type RequestBody = void - type StatusCode = 200 - type ResponseBody = Ordo.User.Public.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace GetUserByID { - type Path = `/users/id/${string}` - type Method = "GET" - type Cookies = void - type Params = { id?: string } - type RequestBody = void - type StatusCode = 200 - type ResponseBody = Ordo.User.Public.DTO - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace RefreshToken { - type Path = "/account/refresh-token" - type Method = "POST" - type Cookies = { sub?: string; jti?: string } - type Params = void - type RequestBody = void - type StatusCode = 200 - type ResponseBody = TTokenResult - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace SignIn { - type Path = "/account/sign-in" - type Method = "POST" - type Cookies = void - type Params = void - type StatusCode = 200 - type RequestBody = { email?: string; handle?: string; password?: string } - type ResponseBody = TTokenResult - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace SignUp { - type Path = "/account/sign-up" - type Method = "POST" - type Cookies = void - type Params = void - type RequestBody = { email?: string; handle?: string; password?: string } - type StatusCode = 201 - type ResponseBody = TTokenResult - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode; body: ResponseBody } - } - - namespace SignOut { - type Path = "/account/sign-out" - type Method = "POST" - type Cookies = { sub?: SUB; jti?: JTI } - type Params = void - type RequestBody = void - type StatusCode = 204 - type ResponseBody = void - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode } - } - - namespace VerifyToken { - type Path = "/account/verify-token" - type Method = "POST" - type Cookies = void - type Params = void - type RequestBody = void - type StatusCode = 200 - type ResponseBody = void - - type Request = { - path: Path - method: Method - cookies: Cookies - params: Params - body: RequestBody - } - type Response = { status: StatusCode } - } - } - } } } diff --git a/lib/core/src/user.impl.ts b/lib/core/src/user.impl.ts index 0fe20b92f..b11343c89 100644 --- a/lib/core/src/user.impl.ts +++ b/lib/core/src/user.impl.ts @@ -28,9 +28,9 @@ import { UserSubscription } from "./constants" const can_user_add_function = (dto: Ordo.User.Current.DTO) => () => dto.installed_functions.length < dto.max_functions -const can_user_create_files = (dto: Ordo.User.Current.DTO) => (files: number) => files < dto.file_limit +const can_user_create_files = (dto: Ordo.User.Current.DTO) => (files: number) => files <= dto.file_limit -const can_user_upload = (dto: Ordo.User.Current.DTO) => (bytes: number) => bytes <= dto.max_upload_size +const can_user_upload = (dto: Ordo.User.Current.DTO) => (bytes: number) => bytes <= dto.max_upload_size * 1024 * 1024 const get_user_created_at = (dto: Ordo.User.Public.DTO) => () => new Date(dto.created_at) diff --git a/lib/frontend-app/index.ts b/lib/frontend-app/index.ts index b555fb1f2..55c3f1eb6 100644 --- a/lib/frontend-app/index.ts +++ b/lib/frontend-app/index.ts @@ -33,6 +33,7 @@ import { OrdoTitleDisplay } from "./src/components/title.component" import { OrdoWorkspace } from "./src/components/workspace.component" import { ordo_app_state } from "./app.state" +import { auth_commands } from "./src/jabs/commands/auth.command" import { create_command_palette } from "./src/jabs/create-command-palette.jab" import { create_file_command } from "./src/jabs/commands/create-file.command" import { create_function_state } from "./src/jabs/create-function-state.jab" @@ -42,52 +43,62 @@ import { edit_file_links_command } from "./src/jabs/commands/edit-file-links.com import { move_file_command } from "./src/jabs/commands/move-file.command" import { remove_file_command } from "./src/jabs/commands/remove-file.command" import { rename_file_command } from "./src/jabs/commands/rename-file.command" -import { start_data_orchestrator } from "./src/jabs/start-data-orchestrator.jab" +import { start_metadata_manager } from "./src/jabs/start-data-orchestrator.jab" // TODO Move fonts to assets import "./index.css" -import { auth_commands } from "./src/jabs/commands/auth.command" + +export type TAppOptions = { + dt_host: string + id_host: string + pb_host: string +} // TODO Move translations from file explorer -export const App = Maoka.create("div", ({ use }) => { - const { app_fid } = ordo_app_state.zags.select("constants") +export const App = ({ id_host, dt_host, pb_host }: TAppOptions) => + Maoka.create("div", ({ use }) => { + ordo_app_state.zags.update("hosts.id", () => id_host) + ordo_app_state.zags.update("hosts.dt", () => dt_host) + ordo_app_state.zags.update("hosts.pb", () => pb_host) + + const { app_fid } = ordo_app_state.zags.select("constants") - const { repositories, source } = use(create_function_state_source) - const app_state = use(create_function_state(app_fid, source)) + const { repositories, source } = use(create_function_state_source) + const app_state = use(create_function_state(app_fid, source)) - use(MaokaOrdo.Context.provide(app_state)) - use(start_data_orchestrator(repositories)) + use(MaokaOrdo.Context.provide(app_state)) + use(start_metadata_manager(repositories)) - use(MaokaJabs.set_class("app")) - use(create_command_palette) - use(move_file_command) - use(remove_file_command) - use(create_file_command) - use(rename_file_command) - use(edit_file_labels_command) - use(edit_file_links_command) - use(auth_commands) + use(MaokaJabs.set_class("app")) + use(create_command_palette) + use(move_file_command) + use(remove_file_command) + use(create_file_command) + use(rename_file_command) + use(edit_file_labels_command) + use(edit_file_links_command) + use(auth_commands) - // TODO Render user defined functions - // TODO .catch - void Promise.any([ - import("./src/sections/welcome").then(({ default: f }) => f(source)), - import("./src/sections/file-editor").then(({ default: f }) => f(source)), - import("@ordo-pink/function-rich-text") - .then(({ default: f }) => f(source)) - .then(() => import("@ordo-pink/function-database")) - .then(({ default: f }) => f(source)), - ]) + // TODO Render user defined functions + // TODO .catch + void Promise.any([ + import("./src/sections/welcome").then(({ default: f }) => f(source)), + import("./src/sections/file-editor").then(({ default: f }) => f(source)), + import("@ordo-pink/function-rich-text") + .then(({ default: f }) => f(source)) + .then(() => import("@ordo-pink/function-database")) + .then(({ default: f }) => f(source)), + ]) - // TODO Init user - return () => [ - OrdoWorkspace, - OrdoSidebar, - OrdoModal, - OrdoNotifications, - OrdoContextMenu, - OrdoActivityBar, - OrdoBackgroundTaskIndicator, - OrdoTitleDisplay, - ] -}) + // TODO Init user + return () => [ + OrdoWorkspace, + OrdoSidebar, + OrdoModal, + OrdoNotifications, + OrdoContextMenu, + OrdoActivityBar, + OrdoBackgroundTaskIndicator, + OrdoTitleDisplay, + ] + }) diff --git a/lib/frontend-app/src/components/command-palette/command-palette-item.component.ts b/lib/frontend-app/src/components/command-palette/command-palette-item.component.ts index e001e9036..d42bd8cb7 100644 --- a/lib/frontend-app/src/components/command-palette/command-palette-item.component.ts +++ b/lib/frontend-app/src/components/command-palette/command-palette-item.component.ts @@ -41,7 +41,7 @@ export const OrdoCommandPaletteItem = (item: Ordo.CommandPalette.Item, on_click: : void 0 on_mount(() => { - if (is_current && !is_in_view(element as Element, element.parentElement!)) + if (element instanceof HTMLElement && is_current && !is_in_view(element, element.parentElement!)) element.scrollIntoView?.({ behavior: "smooth", inline: "center", block: "center" }) }) diff --git a/lib/frontend-app/src/data/content/content-persistence-strategy-indexed-db.ts b/lib/frontend-app/src/data/content/content-persistence-strategy-indexed-db.ts new file mode 100644 index 000000000..26588795a --- /dev/null +++ b/lib/frontend-app/src/data/content/content-persistence-strategy-indexed-db.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Oath, ops0 } from "@ordo-pink/oath" +import { IndexedDBStorePromise } from "@ordo-pink/oath-indexeddb" +import { RRR } from "@ordo-pink/core" +import { noop } from "@ordo-pink/tau" + +export const PersistenceStrategyContentIndexedDB = { + Of: ( + db_name: string, + store_name: string, + db_version: number, + on_upgrade_needed: (indexed_db: IDBOpenDBRequest) => (event: IDBVersionChangeEvent) => any, + ): Ordo.Content.PersistenceStrategy => { + const indexed_db = indexedDB.open(db_name, db_version) + + const eio = (rrr: Error) => RRR.codes.eio("IndexedDB Error", rrr) + + const db_promise = new Promise((resolve, reject) => { + indexed_db.onupgradeneeded = on_upgrade_needed(indexed_db) + + indexed_db.onsuccess = (event: any) => { + resolve(event.target.result as IDBDatabase) + } + + indexed_db.onerror = (event: any) => { + reject(event.target.error?.message ?? "Something wrong with IndexedDB") + } + }) + + const store0 = Oath.Resolve(() => db_promise) + .and(f => Oath.FromPromise(f).pipe(ops0.rejected_map(eio))) + .and(db => Oath.FromNullable(db, () => eio(new Error("Could not establish IndexedDB connection")))) + .and(db => Oath.Try(() => db.transaction([store_name], "readwrite"), eio)) + .and(transaction => Oath.Try(() => transaction.objectStore(store_name), eio)) + .and(store => IndexedDBStorePromise.Of(store)) + + return { + clear: () => store0.and(s => s.clear()).pipe(ops0.rejected_map(eio)), + delete: (uid, fsid) => store0.and(s => s.delete(get_path(uid, fsid))).pipe(ops0.rejected_map(eio)), + exists: (uid, fsid) => + store0 + .and(s => s.count(get_path(uid, fsid))) + .and(count => count > 0) + .pipe(ops0.rejected_map(eio)), + get: (uid, fsid) => store0.and(s => s.get(get_path(uid, fsid))).pipe(ops0.rejected_map(eio)), + list: () => + store0 + .and(s => s.get_all_keys()) + .and(keys => Oath.Merge(keys.reduce((acc, key) => ({ ...acc, [key as string]: store0.and(s => s.get(key)) }), {}))) + .pipe(ops0.rejected_map(eio)), + put: (uid, fsid, content) => + store0 + .and(s => s.put(content, get_path(uid, fsid))) + .and(noop) + .pipe(ops0.rejected_map(eio)), + } + }, +} + +const get_path = (uid: Ordo.User.UID, fsid: Ordo.Metadata.FSID) => `${fsid}` diff --git a/lib/frontend-app/src/data/content/content-persistence-strategy-ordo-backend.ts b/lib/frontend-app/src/data/content/content-persistence-strategy-ordo-backend.ts new file mode 100644 index 000000000..efb815fa2 --- /dev/null +++ b/lib/frontend-app/src/data/content/content-persistence-strategy-ordo-backend.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Oath, ops0 } from "@ordo-pink/oath" +import { RRR } from "@ordo-pink/core" +import { TZags } from "@ordo-pink/zags" + +export const PersistenceStrategyContentOrdoBackend = { + Of: ( + dt_host: string, + fetch: Ordo.Fetch, + $: TZags<{ user: Ordo.User.Current.Instance | null; token: string | null }>, + ): Ordo.Content.PersistenceStrategy => { + return { + clear: () => Oath.Reject(RRR.codes.eio("NOT IMPLEMENTED")), + delete: () => Oath.Reject(RRR.codes.eio("NOT IMPLEMENTED")), + exists: () => Oath.Reject(RRR.codes.eio("NOT IMPLEMENTED")), + get: (uid, fsid) => + Oath.FromNullable($.select("token"), () => new Error("User is not authenticated")) + .and(token => fetch(`${dt_host}/${uid}/${fsid}`, { headers: { Authorization: `Bearer ${token}` } })) + .and(res => Oath.If(res.status === 200, { T: () => res })) + .and(res => res.body) + .pipe(ops0.rejected_map((e: Error) => RRR.codes.eio(e?.message, e))), + list: () => Oath.Reject(RRR.codes.eio("NOT IMPLEMENTED")), + put: (uid, fsid, body) => + Oath.FromNullable($.select("token"), () => new Error("User is not authenticated")) + .and(token => + fetch(`${dt_host}/${uid}/${fsid}`, { headers: { Authorization: `Bearer ${token}` }, method: "PUT", body }), + ) + .and(res => res.json()) + .and(res => Oath.If(res.success)) + .pipe(ops0.rejected_map((e: Error) => RRR.codes.eio(e?.message, e))), + } + }, +} diff --git a/lib/frontend-app/src/data/content/content-query.impl.ts b/lib/frontend-app/src/data/content/content-query.impl.ts index e518a61dc..9f992a04f 100644 --- a/lib/frontend-app/src/data/content/content-query.impl.ts +++ b/lib/frontend-app/src/data/content/content-query.impl.ts @@ -24,7 +24,6 @@ import { Oath } from "@ordo-pink/oath" // TODO Move to frontend-app export const ContentQuery: Ordo.Content.QueryStatic = { Of: (repo: Ordo.Content.Repository, check_query_permission) => ({ - get: (fsid, content_type) => - check_query_permission("content.get").cata({ Ok: () => repo.get(fsid, content_type), Err: x => Oath.Reject(x) }), + get: (uid, fsid) => check_query_permission("content.get").cata({ Ok: () => repo.get(uid, fsid), Err: x => Oath.Reject(x) }), }), } diff --git a/lib/frontend-app/src/data/content/content-repository.impl.ts b/lib/frontend-app/src/data/content/content-repository.impl.ts index c3d7551f4..b8ce1f5ae 100644 --- a/lib/frontend-app/src/data/content/content-repository.impl.ts +++ b/lib/frontend-app/src/data/content/content-repository.impl.ts @@ -19,61 +19,165 @@ * along with this program. If not, see . */ -import { Oath, ops0 } from "@ordo-pink/oath" -import { RRR } from "../../../../core/src/rrr" +import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { METADATA_CONTENT_FSID } from "@ordo-pink/core" +import { T } from "@ordo-pink/tau" +import { ZAGS } from "@ordo-pink/zags" -const INDEXEDDB_NAME = "ordo" -const INDEXEDDB_OBJECT_STORE_NAME = "ordo_db" -const INDEXEDDB_OBJECT_STORE_VERSION = 3 +// TODO Sync storages +export const ContentRepository: Ordo.Content.RepositoryStatic = { + Of: (auth$, local_strategy, remote_strategy) => { + const $ = ZAGS.Of({ version: 0 }) -// TODO Move to frontend-app -// TODO Persistence strategy support -export const CacheContentRepository: Ordo.Content.RepositoryStatic = { - Of: () => { - const indexed_db = indexedDB.open(INDEXEDDB_NAME, INDEXEDDB_OBJECT_STORE_VERSION) + const divorce = auth$.marry(({ user, token }) => { + if (!token || !user) return - const db_promise = new Promise((resolve, reject) => { - indexed_db.onupgradeneeded = () => { - const db = indexed_db.result - if (!db.objectStoreNames.contains(INDEXEDDB_OBJECT_STORE_NAME)) db.createObjectStore(INDEXEDDB_OBJECT_STORE_NAME) - } + // Check if remote state and current state are equal + void Oath.Merge({ + remote: remote_strategy + .get(user.get_id(), METADATA_CONTENT_FSID) + .and(Oath.FromNullable) + .and(stream => new Response(stream as ReadableStream).json() as Promise) + .fix(() => []), + local: local_strategy + .get(user.get_id(), METADATA_CONTENT_FSID) + .and(Oath.FromNullable) + .and(content => Oath.Try(() => JSON.parse(content as string) as Ordo.Metadata.DTO[])) + .fix(() => []), + }) + // Filter out unchanged items to avoid redundant pending updates + .and(({ remote, local }) => ({ + remote_sorted: remote.toSorted((a, b) => (a.updated_at < b.updated_at ? -1 : a.updated_at > b.updated_at ? 1 : 0)), + local_sorted: local.toSorted((a, b) => (a.updated_at < b.updated_at ? -1 : a.updated_at > b.updated_at ? 1 : 0)), + })) + // Collect diffs + .and(({ remote_sorted, local_sorted }) => { + const local_update = [] as Ordo.Metadata.DTO[] + const remote_update = [] as Ordo.Metadata.DTO[] + const intersection = [] as Ordo.Metadata.DTO[] - indexed_db.onsuccess = (event: any) => { - resolve(event.target.result as IDBDatabase) - } + if (local_sorted.length === 0 && remote_sorted.length > 0) { + local_update.push(...remote_sorted) + } else if (local_sorted.length > 0 && remote_sorted.length === 0) { + remote_update.push(...local_sorted) + } else { + let li = 0 + let ri = 0 - indexed_db.onerror = (event: any) => { - reject(event.target.error?.message ?? "Something wrong with IndexedDB") - } + while (li < local_sorted.length && ri < remote_sorted.length) { + const current_local_item = local_sorted[li] + const current_remote_item = remote_sorted[ri] + const remote_item = remote_sorted.find(i => i.fsid === current_local_item.fsid) + const local_item = local_sorted.find(i => i.fsid === current_remote_item.fsid) + + if (!remote_item || current_local_item.updated_at > remote_item.updated_at) { + remote_update.push(current_local_item) + li++ + } else if (!local_item || current_remote_item.updated_at > local_item.updated_at) { + local_update.push(current_remote_item) + ri++ + } else { + intersection.push(current_local_item) + li++ + ri++ + } + } + } + + return { intersection, local_update, remote_update } + }) + .and(({ intersection, local_update, remote_update }) => ({ + local: + local_update.length > 0 && + intersection + .concat(remote_update.filter(remote_item => !local_update.some(i => i.fsid === remote_item.fsid))) + .concat(local_update), + remote: + remote_update.length > 0 && + intersection + .concat(local_update.filter(local_item => !remote_update.some(i => i.fsid === local_item.fsid))) + .concat(remote_update), + })) + .and(({ local, remote }) => + Oath.Merge({ + local: local && local_strategy.put(user.get_id(), METADATA_CONTENT_FSID, JSON.stringify(local)).and(T), + remote: remote && remote_strategy.put(user.get_id(), METADATA_CONTENT_FSID, JSON.stringify(remote)).and(T), + }), + ) + .and(({ local }) => + Oath.If(local) + .pipe(ops0.tap(() => $.update("version", v => v + 1))) + .fix(() => void 0), + ) + + .invoke(invokers0.to_promise) + + divorce() }) return { - get: fsid => - Oath.FromPromise(() => db_promise) - .pipe(ops0.chain(db => Oath.FromNullable(db))) - .pipe(ops0.rejected_map(rrr => RRR.codes.eio("Failed to access IndexedDB cache", rrr))) - .pipe(ops0.chain(db => Oath.Try(() => db.transaction(INDEXEDDB_OBJECT_STORE_NAME, "readonly")))) - .pipe(ops0.map(transaction => transaction.objectStore(INDEXEDDB_OBJECT_STORE_NAME))) - .pipe(ops0.map(storage => storage.get(fsid))) - .pipe(ops0.chain(dbr => new Oath(res => void (dbr.onsuccess = event => res((event.target as any)?.result ?? null))))) - .fix(() => null) as any, // TODO Fix types + get: (uid, fsid) => local_strategy.get(uid, fsid).fix(() => null), + get_all: () => local_strategy.list(), + put: (uid, fsid, content) => local_strategy.put(uid, fsid, content).and(() => remote_strategy.put(uid, fsid, content)), + get $() { + return $ + }, + } + }, +} - put: (fsid, content) => - Oath.FromPromise(() => db_promise) - .pipe(ops0.chain(db => Oath.FromNullable(db))) - .pipe(ops0.rejected_map(() => RRR.codes.eio("Failed to access cache inside IndexedDB"))) - .pipe(ops0.map(db => db.transaction(INDEXEDDB_OBJECT_STORE_NAME, "readwrite"))) - .pipe(ops0.map(transaction => transaction.objectStore(INDEXEDDB_OBJECT_STORE_NAME))) - .pipe(ops0.map(storage => storage.put(content, fsid))) - .pipe( - ops0.chain( - result => - new Oath((resolve, reject) => { - result.onsuccess = () => resolve(void 0) - result.onerror = () => reject(RRR.codes.eio("Failed to access cache inside IndexedDB")) +/* +.and(() => { + const last_local = metadata_repository + .get() + .pipe( + Result.ops.map(items => + items.reduce( + (acc, v) => (acc ? (v.get_updated_at() > acc ? v.get_updated_at() : acc) : v.get_updated_at()), + null as Date | null, + ), + ), + ) + .pipe(Result.ops.chain(Result.FromNullable)) + .cata(Result.catas.or_else(() => new Date(1970, 1, 2))) + + const fetch = ordo_app_state.zags.select("fetch") + const token = ordo_app_state.zags.select("auth.token") + const user = ordo_app_state.zags.select("auth.user") + + if (!user || !token) return + + void Oath.Try(() => + fetch(`${dt_host}/${user.get_id()}/${METADATA_CONTENT_FSID}`, { + headers: { Authorization: `Bearer ${token}` }, + method: "HEAD", + }), + ) + .and(res => Oath.FromNullable(res.headers.get("last-modified"))) + .and(str => new Date(str)) + .and(Oath.FromNullable) + .and(date => Oath.If(is_date(date))) + .fix(() => new Date(1970, 1, 1)) + .and(last_remote => { + if (last_remote! < last_local) { + // TODO Put all content + return content_repository.get_all().and(items => + Oath.Merge( + keys_of(items).map(key => + Oath.Try(() => + fetch(`${dt_host}/${user.get_id()}/${key}`, { + method: "PUT", + headers: { Authorization: `Bearer ${token}` }, + body: items[key], }), + ), ), ), - } - }, -} + ) + } else if (last_remote! > last_local) { + // TODO Pull all content + } + }) + .invoke(invokers0.force_resolve) +}) +*/ diff --git a/lib/frontend-app/src/data/metadata/metadata-command.types.ts b/lib/frontend-app/src/data/metadata/metadata-command.types.ts index 9e169ed3f..422ef38fa 100644 --- a/lib/frontend-app/src/data/metadata/metadata-command.types.ts +++ b/lib/frontend-app/src/data/metadata/metadata-command.types.ts @@ -30,7 +30,7 @@ export type TMetadataCommandStatic = { of: TMetadatCommandConstructor } export type TMetadataCommand = { create: ( - params: Ordo.Metadata.CreateParams & { author_id: Ordo.User.ID }, + params: Ordo.Metadata.CreateParams & { author_id: Ordo.User.UID }, ) => TResult> replace: (value: Ordo.Metadata.Instance) => TResult> diff --git a/lib/frontend-app/src/data/metadata/metadata-repository.impl.ts b/lib/frontend-app/src/data/metadata/metadata-repository.impl.ts index 3e0a6d07f..15e44a25f 100644 --- a/lib/frontend-app/src/data/metadata/metadata-repository.impl.ts +++ b/lib/frontend-app/src/data/metadata/metadata-repository.impl.ts @@ -74,7 +74,6 @@ export const CacheMetadataRepository: Ordo.Metadata.RepositoryAsyncStatic = { Oath.FromPromise(() => result_p) .pipe(ops0.chain(db => Oath.FromNullable(db))) .pipe(ops0.rejected_map(rrr => RRR.codes.eio("Failed to access IndexedDB cache", rrr))) - .pipe(ops0.rejected_tap(console.log)) .pipe(ops0.map(db => db.transaction("metadata", "readonly"))) .pipe(ops0.map(transaction => transaction.objectStore("metadata"))) .pipe(ops0.map(storage => storage.get("items"))) diff --git a/lib/frontend-app/src/data/user/user-query.impl.ts b/lib/frontend-app/src/data/user/user-query.impl.ts index 974534b4a..c17370694 100644 --- a/lib/frontend-app/src/data/user/user-query.impl.ts +++ b/lib/frontend-app/src/data/user/user-query.impl.ts @@ -19,49 +19,61 @@ * along with this program. If not, see . */ +import { Oath, ops0 } from "@ordo-pink/oath" +import { RRR } from "@ordo-pink/core" import { Result } from "@ordo-pink/result" -import { ops0 } from "@ordo-pink/oath" - -import { CurrentUser } from "../../../../core/src/user.impl" -import { UserSubscription } from "../../../../core/src/constants" import { ZAGS } from "@ordo-pink/zags" -const UNKNOWN_USER_ID = "58c0d190-0fe5-4daf-be12-5a1ad0b08edc" - -// TODO Move to frontend-app -const john_doe: Ordo.User.Current.Instance = CurrentUser.FromDTO({ - created_at: Date.now(), - email: "john_doe@ordo.pink", - file_limit: -1, - handle: "@johndoe", - id: UNKNOWN_USER_ID, - installed_functions: [], - max_functions: 10, - max_upload_size: -1, - subscription: UserSubscription.FREE, - first_name: "John", - last_name: "Doe", -}) +import { CurrentUser } from "../../../../core/src/user.impl" +import { ordo_app_state } from "../../../app.state" export const UserQuery: Ordo.User.QueryStatic = { - Of: (c_repo, k_repo) => { + Of: check_permission => { const version_zags = ZAGS.Of({ version: 0 }) - c_repo.$.marry((_, is_update) => is_update && version_zags.update("version", i => i + 1)) - k_repo.$.marry((_, is_update) => is_update && version_zags.update("version", i => i + 1)) + ordo_app_state.zags.cheat("auth.user", (_, is_update) => is_update && version_zags.update("version", i => i + 1)) + + const fetch = ordo_app_state.zags.select("fetch") + const id_host = ordo_app_state.zags.select("hosts.id") + const token = ordo_app_state.zags.select("auth.token") return { - get_current: () => c_repo.get().cata({ Ok: Result.Ok, Err: () => Result.Ok(john_doe) }), + is_authenticated: () => + check_permission("user.is_authenticated") + .pipe(Result.ops.map(() => ordo_app_state.zags.select("auth.user"))) + .pipe(Result.ops.map(user => !!user)) + .cata(Result.catas.or_else(() => false)), + + get_current: () => + check_permission("user.get_current").pipe( + Result.ops.chain(() => Result.FromNullable(ordo_app_state.zags.select("auth.user"))), + ), + get_by_id: id => - k_repo - .get() - .pipe(ops0.map(users => users.find(u => u.get_id() === id) ?? null)) - .pipe( - ops0.tap(o => { - if (!o) { - // TODO: Go get remote - } - }), - ), + check_permission("user.get_by_id") + .cata({ Ok: () => Oath.Resolve(void 0), Err: rrr => Oath.Reject, void>(rrr) }) + .and(() => + Oath.If(CurrentUser.Validations.is_id(id)) + .and(() => id) + .pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid user id"))), + ) + .and(id => ({ id, headers: { Authorization: `Bearer ${token}` } })) + .and(({ id, headers }) => Oath.FromPromise(() => fetch(`${id_host}/users/${id}`, { headers }))) + .and(res => res.json()) + .and(res => Oath.If(res.success, { T: () => res.payload, F: () => RRR.codes.eio(res.payload) })), + + get_by_handle: handle => + check_permission("user.get_by_id") + .cata({ Ok: () => Oath.Resolve(void 0), Err: rrr => Oath.Reject, void>(rrr) }) + .and(() => + Oath.If(CurrentUser.Validations.is_handle(handle)) + .and(() => handle) + .pipe(ops0.rejected_map(() => RRR.codes.einval("Invalid user handle"))), + ) + .and(handle => ({ handle, headers: { Authorization: `Bearer ${token}` } })) + .and(({ handle, headers }) => Oath.FromPromise(() => fetch(`${id_host}/users/handle/${handle}`, { headers }))) + .and(res => res.json()) + .and(res => Oath.If(res.success, { T: () => res.payload, F: () => RRR.codes.eio(res.payload) })), + get $() { return version_zags }, diff --git a/lib/frontend-app/src/data/user/user-repository.impl.ts b/lib/frontend-app/src/data/user/user-repository.impl.ts deleted file mode 100644 index 442122bab..000000000 --- a/lib/frontend-app/src/data/user/user-repository.impl.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { Oath, ops0 } from "@ordo-pink/oath" -import { Result } from "@ordo-pink/result" - -import { CurrentUser } from "../../../../core/src/user.impl" -import { RRR } from "../../../../core/src/rrr" -import { ZAGS } from "@ordo-pink/zags" - -// TODO Move to frontend-app -export const CurrentUserRepository: Ordo.User.Current.RepositoryStatic = { - Of: $ => { - const version_zags = ZAGS.Of({ version: 0 }) - $.marry((_, is_update) => is_update && version_zags.update("version", i => i + 1)) - - return { - get: () => Result.FromNullable($.select("user"), () => RRR.codes.eagain("Loading")), - put: user => Result.Try(() => $.update("user", () => user)), // TODO: - get $() { - return version_zags - }, - } - }, -} - -export const PublicUserRepository: Ordo.User.Public.RepositoryStatic = { - Of: $ => { - const version_zags = ZAGS.Of({ version: 0 }) - $.marry((_, is_update) => is_update && version_zags.update("version", i => i + 1)) - - return { - get: () => Oath.Resolve($.select("known_users")), - put: () => Oath.Reject(RRR.codes.einval("TODO: NOT IMPLEMENTED")), - get $() { - return version_zags - }, - } - }, -} - -export const CurrentUserRepositoryAsync: Ordo.User.Current.RepositoryAsyncStatic = { - Of: (id_host, fetch) => ({ - get: token => - Oath.Resolve("/account" satisfies Ordo.Routes.ID.GetAccount.Path) - .pipe(ops0.map(path => id_host.concat(path))) - .pipe(ops0.chain(url => Oath.FromPromise(() => fetch(url, create_request_init(token))))) - .pipe(ops0.chain(response => Oath.FromPromise(() => response.json()))) - .pipe(ops0.rejected_map(error => RRR.codes.eio(error.message))) - .pipe(ops0.chain(get_response_result0)) - .pipe(ops0.chain(validate_dto0)) - .pipe(ops0.map(CurrentUser.FromDTO)), - - put: () => Oath.Reject(RRR.codes.eio("TODO: UNIMPLEMENTED")), - }), -} - -const create_request_init = (token: string) => ({ headers: { Authorization: `Bearer ${token}` } }) - -const validate_dto0 = (result: unknown) => - Oath.If(CurrentUser.Validations.is_dto(result), { - T: () => result as Ordo.User.Current.DTO, - F: () => RRR.codes.einval("Entity is not a user DTO", result), - }) - -const get_response_result0 = (response_body: any) => - Oath.If(response_body.success, { - T: () => response_body.result, - F: () => RRR.codes.einval("Request error occured", response_body.error), - }) diff --git a/lib/frontend-app/src/frontend-app.content.ts b/lib/frontend-app/src/frontend-app.content.ts index 2c50e565d..d75223cce 100644 --- a/lib/frontend-app/src/frontend-app.content.ts +++ b/lib/frontend-app/src/frontend-app.content.ts @@ -25,23 +25,45 @@ import { is_instance_of, is_string } from "@ordo-pink/tau" import { ConsoleLogger } from "@ordo-pink/logger" import { R } from "@ordo-pink/result" import { Switch } from "@ordo-pink/switch" +import { ZAGS } from "@ordo-pink/zags" -import { CacheContentRepository } from "./data/content/content-repository.impl" import { ContentQuery } from "./data/content/content-query.impl" +import { ContentRepository } from "./data/content/content-repository.impl" +import { PersistenceStrategyContentIndexedDB } from "./data/content/content-persistence-strategy-indexed-db" +import { PersistenceStrategyContentOrdoBackend } from "./data/content/content-persistence-strategy-ordo-backend" import { ordo_app_state } from "../app.state" +const INDEXEDDB_NAME = "ordo" +const INDEXEDDB_OBJECT_STORE_NAME = "ordo_db" +const INDEXEDDB_OBJECT_STORE_VERSION = 3 + type TF = () => { content_repository: Ordo.Content.Repository; get_content_query: (fid: symbol) => Ordo.Content.Query } export const init_content: TF = () => { const logger = ordo_app_state.zags.select("logger") const commands = ordo_app_state.zags.select("commands") - const fetch = ordo_app_state.zags.select("fetch") - const hosts = ordo_app_state.zags.select("hosts") const known_functions = ordo_app_state.zags.select("known_functions") const app_fid = ordo_app_state.zags.select("constants.app_fid") + const dt_host = ordo_app_state.zags.select("hosts.dt") + const fetch = ordo_app_state.zags.select("fetch") logger.debug("🟡 Initialising metadata...") - const content_repository = CacheContentRepository.Of(hosts.dt, fetch) + const local_strategy = PersistenceStrategyContentIndexedDB.Of( + INDEXEDDB_NAME, + INDEXEDDB_OBJECT_STORE_NAME, + INDEXEDDB_OBJECT_STORE_VERSION, + indexed_db => () => { + const db = indexed_db.result + if (!db.objectStoreNames.contains(INDEXEDDB_OBJECT_STORE_NAME)) db.createObjectStore(INDEXEDDB_OBJECT_STORE_NAME) + }, + ) + + const auth$ = ZAGS.Of({ token: null as string | null, user: null as Ordo.User.Current.Instance | null }) + ordo_app_state.zags.cheat("auth.user", user => auth$.update("user", () => user)) + ordo_app_state.zags.cheat("auth.token", token => auth$.update("token", () => token)) + + const remote_strategy = PersistenceStrategyContentOrdoBackend.Of(dt_host, fetch, auth$) + const content_repository = ContentRepository.Of(auth$, local_strategy, remote_strategy) // TODO Extract for common error handling const Err = (rrr: Ordo.Rrr) => { @@ -59,13 +81,14 @@ export const init_content: TF = () => { commands.on("cmd.content.set", ({ fsid, content }) => { const metadata_query = ordo_app_state.zags.select("queries.metadata") const size = get_size(content) + const user = ordo_app_state.zags.select("auth.user") - if (size > 0) { + if (user && size > 0) { // TODO Check if metadata exists void metadata_query.get_by_fsid(fsid).cata( R.catas.if_ok(() => content_repository - .put(fsid, content) + .put(user.get_id(), fsid, content) .pipe(ops0.tap(() => commands.emit("cmd.metadata.set_size", { fsid, size }))) .invoke(invokers0.or_else(Err)), ), @@ -91,9 +114,12 @@ export const init_content: TF = () => { } const fsid = metadata.get_fsid() + const user = ordo_app_state.zags.select("auth.user") + + if (!user) return void content_repository - .put(metadata?.get_fsid(), content) + .put(user.get_id(), metadata.get_fsid(), content) .pipe(ops0.tap(() => commands.emit("cmd.metadata.set_size", { fsid, size }))) .invoke(invokers0.or_else(Err)) }) diff --git a/lib/frontend-app/src/frontend-app.fetch.ts b/lib/frontend-app/src/frontend-app.fetch.ts index 87a0e539d..31ba86d6b 100644 --- a/lib/frontend-app/src/frontend-app.fetch.ts +++ b/lib/frontend-app/src/frontend-app.fetch.ts @@ -36,6 +36,8 @@ export const init_fetch: TF = call_once(() => { logger.debug("🟢 Initialised fetch.") + ordo_app_state.zags.update("fetch", () => fetch) + return { get_fetch: fid => R.If(known_functions.has_permissions(fid, { queries: ["application.fetch"] })) diff --git a/lib/frontend-app/src/frontend-app.data-manager.ts b/lib/frontend-app/src/frontend-app.metadata-manager.ts similarity index 50% rename from lib/frontend-app/src/frontend-app.data-manager.ts rename to lib/frontend-app/src/frontend-app.metadata-manager.ts index 09aa64a71..0200f3d8f 100644 --- a/lib/frontend-app/src/frontend-app.data-manager.ts +++ b/lib/frontend-app/src/frontend-app.metadata-manager.ts @@ -19,25 +19,42 @@ * along with this program. If not, see . */ +// TODO Publish file as a new file +// TODO Access file by user_handle and prop link (e.g. https://pub.ordo.pink/@ordo-blog/en/release-0.8.0) +// ^-----pub host------^ ^----user---^ ^--file_id---^ + +import { METADATA_CONTENT_FSID, Metadata } from "@ordo-pink/core" import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { is_array, is_string } from "@ordo-pink/tau" import { Result } from "@ordo-pink/result" -import { Metadata } from "../../core/src/metadata.impl" - -// TODO Think at least a bit! -const METADATA_CONTENT_FSID = "b68678af-6776-47c0-a0b8-4b6535664b8c" +import { ordo_app_state } from "../app.state" -export const DataManager = { +export const MetadataManager = { Of: (metadata_repository: Ordo.Metadata.Repository, content_repository: Ordo.Content.Repository): TMetadataManager => { const get_metadata_content0 = content_repository - .get(METADATA_CONTENT_FSID, "text") + .get("" as any, METADATA_CONTENT_FSID) .and(Oath.FromNullable) + .and(content => Oath.If(is_string(content), { T: () => content as string })) .and(content => Oath.Try(() => JSON.parse(content) as Ordo.Metadata.DTO[])) .fix(() => [] as Ordo.Metadata.DTO[]) .and(dtos => dtos.map(Metadata.FromDTO)) .and(metadata_repository.put) .and(result => result.cata({ Ok: () => Oath.Resolve(void 0), Err: Oath.Reject })) + content_repository.$.marry( + (_, is_update) => + is_update && + void content_repository + .get("" as any, METADATA_CONTENT_FSID) + .and(stream => new Response(stream)) + .and(res => res.json()) + .and(items => Oath.If(is_array(items), { T: () => items })) + .and(items => items.map(Metadata.FromDTO)) + .and(json => metadata_repository.put(json)) + .invoke(invokers0.to_promise), + ) + let divorce_metadata_repository: () => void let cancel_get_content: () => void @@ -65,9 +82,13 @@ export const DataManager = { if (!dtos) return // TODO Log error, do stuff - previous_save_attempt0 = Oath.Resolve(on_state_change("put-remote")) - .and(() => Oath.Try(() => JSON.stringify(dtos))) - .and(str => content_repository.put(METADATA_CONTENT_FSID, str)) + const user = ordo_app_state.zags.select("auth.user") + + if (user) { + previous_save_attempt0 = Oath.Resolve(on_state_change("put-remote")) + .and(() => Oath.Try(() => JSON.stringify(dtos))) + .and(str => content_repository.put(user.get_id(), METADATA_CONTENT_FSID, str)) + } void previous_save_attempt0 .pipe(ops0.bitap(mark_put_complete, mark_put_complete)) @@ -98,85 +119,3 @@ export type TMetadataManager = { start: (on_state_change: (change: TMetadataManagerStateChange) => void) => Promise cancel: () => void } - -// export const MetadataManager: TMetadataManagerStatic = { -// of: (l_repo, r_repo /*, auth$ */) => ({ -// start: on_state_change => { -// void Oath.Resolve("") -// .pipe(ops0.tap(() => on_state_change("get-remote"))) -// .pipe(ops0.chain(r_repo.get)) -// .pipe(ops0.tap(() => on_state_change("get-remote-complete"))) -// .pipe(ops0.map(metadata => metadata.map(item => Metadata.FromDTO(item)))) -// .pipe(ops0.map(metadata => l_repo.put(metadata))) -// .pipe(ops0.chain(res => res.cata({ Ok: Oath.Resolve, Err: Oath.Reject }))) -// .invoke( -// invokers0.or_else(() => { -// // TODO: Handle errors -// on_state_change("get-remote-complete") -// }), -// ) -// // TODO: Cache/offline repo - -// l_repo.$.subscribe(i => { -// if (i < 1) return - -// void Oath.Resolve(l_repo.get()) -// .pipe(ops0.chain(res => res.cata({ Ok: Oath.Resolve, Err: Oath.Reject }))) -// .pipe(ops0.map(metadata => metadata.map(item => item.to_dto()))) -// .pipe(ops0.tap(() => on_state_change("put-remote"))) -// .pipe(ops0.chain(metadata => r_repo.put("", metadata))) -// .pipe(ops0.tap(() => on_state_change("put-remote-complete"))) -// .invoke( -// invokers0.or_else(() => { -// // TODO: Handle errors -// on_state_change("put-remote-complete") -// }), -// ) -// }) -// }, -// }), -// auth$ -// .pipe(map(auth_option => O.FromNullable(auth_option.unwrap()?.token))) -// .pipe(combineLatestWith(l_repo.$)) -// .pipe( -// map(([token_option, iteration]) => { -// if (iteration === 0) { -// void Oath.FromNullable(token_option.unwrap()) -// .pipe(ops0.tap(() => on_state_change("get-remote"))) -// .pipe(ops0.chain(r_repo.get)) -// .pipe(ops0.tap(() => on_state_change("get-remote-complete"))) -// .pipe(ops0.map(metadata => metadata.map(item => Metadata.FromDTO(item)))) -// .pipe(ops0.map(metadata => l_repo.put(metadata))) -// .pipe(ops0.chain(res => res.cata({ Ok: Oath.Resolve, Err: Oath.Reject }))) -// .invoke( -// invokers0.or_else(() => { -// // TODO: Handle errors -// on_state_change("get-remote-complete") -// }), -// ) - -// return -// } - -// // TODO: Patch changes -// token_option.cata({ -// Some: token => -// void Oath.Resolve(l_repo.get()) -// .pipe(ops0.chain(res => res.cata({ Ok: Oath.Resolve, Err: Oath.Reject }))) -// .pipe(ops0.map(metadata => metadata.map(item => item.to_dto()))) -// .pipe(ops0.tap(() => on_state_change("put-remote"))) -// .pipe(ops0.chain(metadata => r_repo.put(token, metadata))) -// .pipe(ops0.tap(() => on_state_change("put-remote-complete"))) -// .invoke( -// invokers0.or_else(() => { -// // TODO: Handle errors -// on_state_change("put-remote-complete") -// }), -// ), -// None: noop, -// }) -// }), -// ) -// .subscribe(), -// }), -// } diff --git a/lib/frontend-app/src/frontend-app.metadata.ts b/lib/frontend-app/src/frontend-app.metadata.ts index 49b2377aa..8d9edb69c 100644 --- a/lib/frontend-app/src/frontend-app.metadata.ts +++ b/lib/frontend-app/src/frontend-app.metadata.ts @@ -93,8 +93,6 @@ export const init_metadata: TInitMetadataFn = call_once(() => { metadata_command.update_label(old_label, new_label).cata({ Ok: noop, Err }), ) - logger.debug("🟢 Initialised metadata.") - const get_metadata_query = (fid: symbol) => MetadataQuery.Of(metadata_repository, permission => Result.If(known_functions.has_permissions(fid, { queries: [permission] }), { @@ -110,6 +108,8 @@ export const init_metadata: TInitMetadataFn = call_once(() => { ordo_app_state.zags.update("queries.metadata", () => app_metadata_query) + logger.debug("🟢 Initialised metadata.") + return { metadata_repository, get_metadata_query } }) diff --git a/lib/frontend-app/src/frontent-app.user.ts b/lib/frontend-app/src/frontent-app.user.ts index a3271c84c..2e2dc1372 100644 --- a/lib/frontend-app/src/frontent-app.user.ts +++ b/lib/frontend-app/src/frontent-app.user.ts @@ -22,10 +22,8 @@ import { ConsoleLogger } from "@ordo-pink/logger" import { RRR } from "@ordo-pink/core" import { Result } from "@ordo-pink/result" -import { ZAGS } from "@ordo-pink/zags" import { call_once } from "@ordo-pink/tau" -import { CurrentUserRepository, PublicUserRepository } from "./data/user/user-repository.impl" import { UserQuery } from "./data/user/user-query.impl" import { ordo_app_state } from "../app.state" @@ -34,12 +32,7 @@ export const init_user = call_once(() => { logger.debug("🟡 Initialising metadata...") - const current_user_repository = CurrentUserRepository.Of(current_user$) - const public_user_repository = PublicUserRepository.Of(public_user$) - - // TODO Auth commands - - const user_query = UserQuery.Of(current_user_repository, public_user_repository, () => Result.Ok(void 0)) + const user_query = UserQuery.Of(() => Result.Ok(void 0)) ordo_app_state.zags.update("queries.user", () => user_query) @@ -47,7 +40,7 @@ export const init_user = call_once(() => { return { get_user_query: (fid: symbol) => - UserQuery.Of(current_user_repository, public_user_repository, permission => + UserQuery.Of(permission => Result.If(known_functions.has_permissions(fid, { queries: [permission] }), { F: () => { const rrr = RRR.codes.eperm( @@ -60,9 +53,3 @@ export const init_user = call_once(() => { ), } }) - -// --- Internal --- - -// TODO Move to ordo_app_state -const current_user$ = ZAGS.Of({ user: null as Ordo.User.Current.Instance | null }) -const public_user$ = ZAGS.Of({ known_users: [] as Ordo.User.Public.Instance[] }) diff --git a/lib/frontend-app/src/jabs/commands/auth.command.ts b/lib/frontend-app/src/jabs/commands/auth.command.ts index 7f10d40ed..b75099dd1 100644 --- a/lib/frontend-app/src/jabs/commands/auth.command.ts +++ b/lib/frontend-app/src/jabs/commands/auth.command.ts @@ -1,3 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2025 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import { BsBoxArrowInRight, BsBoxArrowRight } from "@ordo-pink/frontend-icons" import { CheckboxInput, Dialog, Input } from "@ordo-pink/maoka-components" import { CommandPaletteItemType, CurrentUser } from "@ordo-pink/core" @@ -27,13 +48,22 @@ export const auth_commands: TMaokaJab = ({ onunmount, use }) => { const commands = use(MaokaOrdo.Jabs.get_commands) const fetch = use(MaokaOrdo.Jabs.get_fetch) - - R.FromNullable(localStorage.getItem("user")) - .pipe(R.ops.chain(str => R.Try(() => JSON.parse(str)))) - .pipe(R.ops.chain(user => R.If(CurrentUser.Validations.is_dto(user), { T: () => user as Ordo.User.Current.DTO }))) - .pipe(R.ops.chain(user => R.FromNullable(localStorage.getItem("token")).pipe(R.ops.map(token => ({ user, token }))))) - .pipe(R.ops.map(({ user, token }) => ({ user: CurrentUser.FromDTO(user), token }))) - .pipe(R.ops.map(auth => ordo_app_state.zags.update("auth", () => auth))) + const id_host = ordo_app_state.zags.select("hosts.id") + + void Oath.FromNullable(localStorage.getItem("user")) + .and(str => Oath.Try(() => JSON.parse(str))) + .and(user => Oath.If(CurrentUser.Validations.is_dto(user), { T: () => user as Ordo.User.Current.DTO })) + .and(user => ordo_app_state.zags.update("auth.user", () => CurrentUser.FromDTO(user))) + .and(() => Oath.FromNullable(localStorage.getItem("token")).and(refresh_token)) + .invoke(invokers0.force_resolve) + + const refresh_token = (token: string) => + Oath.Resolve({ headers: { Authorization: `Bearer ${token}` }, method: "POST" }) + .and(init => Oath.FromPromise(() => fetch(`${id_host}/tokens/refresh`, init))) + .and(res => res.json()) + .and(res => Oath.If(res.success, { T: () => res.payload as string })) + .and(token => ordo_app_state.zags.update("auth.token", () => token)) + .pipe(ops0.rejected_map(clean_up_auth)) const handle_sign_out = () => Oath.Resolve(new Headers()) @@ -44,9 +74,9 @@ export const auth_commands: TMaokaJab = ({ onunmount, use }) => { .and(() => headers), ) .and(headers => ({ headers, method: "DELETE" })) - .and(init => Oath.Try(() => fetch("http://localhost:3001/tokens/invalidate", init))) + .and(init => Oath.Try(() => fetch(`${id_host}/tokens/invalidate`, init))) .invoke(invokers0.force_resolve) - .then(clean_up_local_storage) + .then(clean_up_auth) .then(() => { const history_length = history.length history.go(-history_length) @@ -72,18 +102,26 @@ export const auth_commands: TMaokaJab = ({ onunmount, use }) => { render_icon: BsBoxArrowInRight, }) + const divorce_user = ordo_app_state.zags.cheat("auth.user", (user, is_update) => { + if (!is_update) return + if (!user) return clean_up_auth() + + void Oath.FromNullable(user) + .and(u => u.to_dto()) + .and(d => Oath.Try(() => JSON.stringify(d))) + .and(s => Oath.Try(() => localStorage.setItem("user", s))) + .invoke(invokers0.force_resolve) + }) + // TODO use other means but localStorage for storing user info - const divorce_auth = ordo_app_state.zags.cheat("auth", (auth, is_update) => { + const divorce_token = ordo_app_state.zags.cheat("auth.token", (token, is_update) => { if (is_update) { - R.FromNullable(auth.user) - .pipe(R.ops.chain(user => R.Try(() => JSON.stringify(user.to_dto())))) - .pipe(R.ops.map(str => localStorage.setItem("user", str))) - .pipe(R.ops.chain(() => R.FromNullable(auth.token))) + R.FromNullable(token) .pipe(R.ops.map(str => localStorage.setItem("token", str))) - .cata(R.catas.or_else(clean_up_local_storage)) + .cata(R.catas.or_else(clean_up_auth)) } - if (auth.token) { + if (token) { if (!is_authenticated) { commands.off("cmd.auth.show_request_code_modal", handle_show_request_code) commands.off("cmd.auth.show_validate_code_modal", handle_show_validate_code) @@ -113,7 +151,8 @@ export const auth_commands: TMaokaJab = ({ onunmount, use }) => { }) onunmount(() => { - divorce_auth() + divorce_token() + divorce_user() commands.off("cmd.auth.show_request_code_modal", handle_show_request_code) commands.off("cmd.auth.show_validate_code_modal", handle_show_validate_code) @@ -127,6 +166,7 @@ const RequestCodeModal = Maoka.create("div", ({ use }) => { const commands = use(MaokaOrdo.Jabs.get_commands) const fetch = use(MaokaOrdo.Jabs.get_fetch) + const id_host = ordo_app_state.zags.select("hosts.id") // TODO Show hint // const t_hint = "We'll send you a magic link that will let you in." // TODO i18n @@ -160,7 +200,7 @@ const RequestCodeModal = Maoka.create("div", ({ use }) => { .and(headers => ({ headers, method: "POST" })) .and(init => ({ ...init, body: JSON.stringify({ email }) })) // TODO Get input from env - .and(init => Oath.FromPromise(() => fetch("http://localhost:3001/codes/request", init))) + .and(init => Oath.FromPromise(() => fetch(`${id_host}/codes/request`, init))) .and(res => res.json()) .and(res => Oath.If(res.success)) .and(() => commands.emit("cmd.auth.show_validate_code_modal", email as Ordo.User.Email)) @@ -191,9 +231,11 @@ const RequestCodeModal = Maoka.create("div", ({ use }) => { }) }) -const clean_up_local_storage = () => { +const clean_up_auth = () => { localStorage.removeItem("user") localStorage.removeItem("token") + history.go(-history.length) + window.location.replace("/") } const RequestCodeModalCheckboxWrapper = Maoka.styled("div", { class: "px-8" }) @@ -207,6 +249,7 @@ const ValidateCodeModal = (email: Ordo.User.Email) => const commands = use(MaokaOrdo.Jabs.get_commands) const fetch = use(MaokaOrdo.Jabs.get_fetch) + const id_host = ordo_app_state.zags.select("hosts.id") const validate = (x: string) => /^\d{6}$/.test(x) @@ -227,7 +270,7 @@ const ValidateCodeModal = (email: Ordo.User.Email) => .and(headers => ({ headers, method: "POST" })) .and(init => ({ ...init, body: JSON.stringify({ email, code }) })) // TODO Get input from env - .and(init => Oath.FromPromise(() => fetch("http://localhost:3001/codes/validate", init))) + .and(init => Oath.FromPromise(() => fetch(`${id_host}/codes/validate`, init))) .and(res => res.json()) .and(res => Oath.If(res.success, { T: () => res.payload })) .and(({ token, user }) => ordo_app_state.zags.update("auth", () => ({ token, user: CurrentUser.FromDTO(user) }))) diff --git a/lib/frontend-app/src/jabs/start-data-orchestrator.jab.ts b/lib/frontend-app/src/jabs/start-data-orchestrator.jab.ts index b89d0f7a9..1b8207bd9 100644 --- a/lib/frontend-app/src/jabs/start-data-orchestrator.jab.ts +++ b/lib/frontend-app/src/jabs/start-data-orchestrator.jab.ts @@ -24,17 +24,17 @@ import { Switch } from "@ordo-pink/switch" import { type TMaokaJab } from "@ordo-pink/maoka" import { noop } from "@ordo-pink/tau" -import { DataManager } from "../frontend-app.data-manager" +import { MetadataManager } from "../frontend-app.metadata-manager" type P = { metadata: Ordo.Metadata.Repository; content: Ordo.Content.Repository } -export const start_data_orchestrator = +export const start_metadata_manager = (repositories: P): TMaokaJab => async ({ use, onunmount }) => { const commands = use(MaokaOrdo.Jabs.get_commands) - const data_manager = DataManager.Of(repositories.metadata, repositories.content) + const metadata_manager = MetadataManager.Of(repositories.metadata, repositories.content) - await data_manager.start(state_change => + await metadata_manager.start(state_change => Switch.Match(state_change) .case("get-remote", () => commands.emit("cmd.application.background_task.start_loading")) .case("put-remote", () => commands.emit("cmd.application.background_task.start_saving")) @@ -44,6 +44,6 @@ export const start_data_orchestrator = ) onunmount(() => { - data_manager.cancel() + metadata_manager.cancel() }) } diff --git a/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-directory.component.ts b/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-directory.component.ts index 080ec815a..283a35876 100644 --- a/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-directory.component.ts +++ b/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-directory.component.ts @@ -36,6 +36,7 @@ const is_fsid = Metadata.Validations.is_fsid // TODO Rewrite with ActionListItem export const FileEditorSidebarDirectory = (metadata: Ordo.Metadata.Instance, depth = 0) => Maoka.create("div", ({ use, refresh }) => { + const ctx = use(MaokaOrdo.Context.consume) const fsid = metadata.get_fsid() const commands = use(MaokaOrdo.Jabs.get_commands) @@ -48,8 +49,13 @@ export const FileEditorSidebarDirectory = (metadata: Ordo.Metadata.Instance, dep refresh() } - const handle_context_menu = (event: MouseEvent) => + const handle_context_menu = (event: MouseEvent) => { + event.preventDefault() + + commands.emit("cmd.metadata.publish", { ctx, fsid: metadata.get_fsid(), name: "/123123" }) + commands.emit("cmd.application.context_menu.show", { event, payload: metadata }) + } use(MaokaJabs.listen("oncontextmenu", event => handle_context_menu(event))) diff --git a/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-file.component.ts b/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-file.component.ts index da8497652..3f1c4139c 100644 --- a/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-file.component.ts +++ b/lib/frontend-app/src/sections/file-editor/components/file-editor-sidebar-file.component.ts @@ -27,12 +27,15 @@ import { MetadataIcon } from "@ordo-pink/maoka-components" // TODO Rewrite with ActionListItem export const FileEditorSidebarFile = (metadata: Ordo.Metadata.Instance, depth = 0) => Maoka.create("div", ({ use }) => { + const ctx = use(MaokaOrdo.Context.consume) const fsid = metadata.get_fsid() const { emit } = use(MaokaOrdo.Jabs.get_commands) const handle_context_menu = (event: MouseEvent) => { event.preventDefault() + emit("cmd.metadata.publish", { ctx, fsid: metadata.get_fsid(), name: "ordo-tasks" }) + emit("cmd.application.context_menu.show", { event, payload: metadata }) } diff --git a/lib/frontend-app/src/sections/file-editor/components/file-editor-workspace-render-picker.component.ts b/lib/frontend-app/src/sections/file-editor/components/file-editor-workspace-render-picker.component.ts index f352efef0..f76e72964 100644 --- a/lib/frontend-app/src/sections/file-editor/components/file-editor-workspace-render-picker.component.ts +++ b/lib/frontend-app/src/sections/file-editor/components/file-editor-workspace-render-picker.component.ts @@ -32,7 +32,7 @@ export const RenderPicker = (metadata: Ordo.Metadata.Instance) => let fa: Ordo.FileAssociation.Instance | null = null const content_query = use(MaokaOrdo.Jabs.get_content_query) - const content0 = content_query.get(metadata_fsid, "text") + const content0 = content_query.get("" as any, metadata_fsid) ordo_app_state.zags.cheat("functions.file_assocs", fas => { if (fa) return diff --git a/lib/frontend-app/src/sections/welcome/pages/landing.page.ts b/lib/frontend-app/src/sections/welcome/pages/landing.page.ts index aed72f804..ba5101e82 100644 --- a/lib/frontend-app/src/sections/welcome/pages/landing.page.ts +++ b/lib/frontend-app/src/sections/welcome/pages/landing.page.ts @@ -172,7 +172,7 @@ const show_cookie_modal = (emit: Ordo.Command.EmitFn) => { message: "t.welcome.landing_page.cookie_banner.message", type: NotificationType.WARN, duration: 15, - render_icon: span => span.appendChild(BsCookie("size-5") as SVGSVGElement), + render_icon: span => void Maoka.dom(span, BsCookie("size-5")), }) is_cookie_modal_shown = true diff --git a/lib/frontend-icons/index.ts b/lib/frontend-icons/index.ts index eb266a52f..caf5c4e90 100644 --- a/lib/frontend-icons/index.ts +++ b/lib/frontend-icons/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Unlicense */ -import { type TMaokaElement } from "@ordo-pink/maoka" +import { Maoka } from "@ordo-pink/maoka" export const BS_FOLDER_2_OPEN = ` @@ -145,20 +145,11 @@ export const BS_COOKIE = ` - (cls: string = "") => { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") - - cls && svg.setAttribute("class", cls) - svg.setAttributeNS(null, "stroke", "currentColor") - svg.setAttributeNS(null, "fill", "currentColor") - svg.setAttributeNS(null, "stroke-width", "0") - svg.setAttributeNS(null, "viewBox", "0 0 16 16") - svg.setAttributeNS(null, "width", "1em") - - svg.innerHTML = children - - return svg as unknown as TMaokaElement - } + (cls: string = "") => + Maoka.html( + "div", + `${children}`, + ) export const BsTags = Icon(` diff --git a/lib/function-database/index.ts b/lib/function-database/index.ts index 789b21b13..e7c0ae2b4 100644 --- a/lib/function-database/index.ts +++ b/lib/function-database/index.ts @@ -20,12 +20,20 @@ */ import { BsFileEarmarkRuled } from "@ordo-pink/frontend-icons" +import { MaokaOrdo } from "@ordo-pink/maoka-ordo-jabs" +import { MaokaStr } from "@ordo-pink/maoka-render-string" +import { Result } from "@ordo-pink/result" import { TwoLetterLocale } from "@ordo-pink/locale" import { create_function } from "@ordo-pink/core" +import { invokers0 } from "@ordo-pink/oath" import { Database } from "./src/database.component" import { type TColumnName } from "./src/database.types" +import core_styles from "@ordo-pink/frontend-app/index.css?inline" +import db_styles from "./src/database.css?inline" +import maoka_components from "@ordo-pink/maoka-components/maoka-components.css?inline" + declare global { interface t { database: { @@ -99,6 +107,9 @@ export default create_function( "metadata.get_outgoing_links", "metadata.get", "metadata.has_children", + "content.get", + "application.fetch", + "user.get_current", ], }, ctx => { @@ -134,8 +145,47 @@ export default create_function( description: "t.database.file_association.description", }, ], - render: ({ metadata, content }) => Database(metadata, content), + render: ({ metadata, content, is_editable }) => Database(metadata, content, is_editable), render_icon: BsFileEarmarkRuled, }) + + commands.on("cmd.metadata.publish", ({ fsid }) => { + const content_query = ctx.content_query + const metadata_query = ctx.metadata_query + const metadata = metadata_query.get_by_fsid(fsid).cata(Result.catas.or_else(() => null)) + const user = ctx.user_query.get_current().cata(Result.catas.or_else(() => null)) + + if (!user || !metadata || metadata.get_type() !== "database/ordo") return // Log cannot publish if unauthed error + + void content_query + .get(user.get_id(), fsid) + .and(content => MaokaOrdo.Components.WithState(ctx, () => Database(metadata, content, false))) + .and(cmp => MaokaOrdo.Components.WithState(ctx, () => cmp)) + .and(cmp => MaokaStr.render(MaokaStr.create_element("div"), cmp)) + .and( + str => ` + + + +${metadata.get_name()} + + + + + + + +${str} + +`, + ) + .and(console.log) + .invoke(invokers0.to_promise) + }) }, ) diff --git a/lib/function-database/src/components/database-table-head.component.ts b/lib/function-database/src/components/database-table-head.component.ts index 066c68af7..9711dd92c 100644 --- a/lib/function-database/src/components/database-table-head.component.ts +++ b/lib/function-database/src/components/database-table-head.component.ts @@ -29,8 +29,8 @@ import { noop } from "@ordo-pink/tau" import { SortingDirection } from "../database.constants" import { database$ } from "../database.state" -export const DatabaseTableHead = (columns: Ordo.I18N.TranslationKey[]) => - TableHead(() => TableHeadRow(() => columns.map(TableHeadCell))) +export const DatabaseTableHead = (columns: Ordo.I18N.TranslationKey[], is_editable: boolean) => + TableHead(() => TableHeadRow(() => columns.map(c => TableHeadCell(c, is_editable)))) // --- Internal --- @@ -38,12 +38,13 @@ const TableHead = Maoka.styled("thead") const TableHeadRow = Maoka.styled("tr", { class: "database_table-head_row" }) // TODO: Add icons -const TableHeadCell = (column: Ordo.I18N.TranslationKey) => +const TableHeadCell = (column: Ordo.I18N.TranslationKey, is_editable: boolean) => Maoka.create("th", ({ use }) => { const get_db_state = use(MaokaOrdo.Jabs.happy_marriage$(database$)) use(MaokaJabs.add_class("database_table-head_cell")) use(MaokaJabs.listen("onclick", () => handle_click())) + if (is_editable) use(MaokaJabs.add_class("pointable")) const { t } = use(MaokaOrdo.Jabs.get_translations$) const commands = use(MaokaOrdo.Jabs.get_commands) diff --git a/lib/function-database/src/components/database-table-row.component.ts b/lib/function-database/src/components/database-table-row.component.ts index 093c3f4a9..cfb1ac308 100644 --- a/lib/function-database/src/components/database-table-row.component.ts +++ b/lib/function-database/src/components/database-table-row.component.ts @@ -27,14 +27,14 @@ import { R } from "@ordo-pink/result" import { Switch } from "@ordo-pink/switch" import { noop } from "@ordo-pink/tau" -export const DatabaseTableRow = (columns: Ordo.I18N.TranslationKey[], child: Ordo.Metadata.Instance) => +export const DatabaseTableRow = (columns: Ordo.I18N.TranslationKey[], child: Ordo.Metadata.Instance, is_editable: boolean) => Maoka.create("tr", ({ use }) => { use(MaokaJabs.set_class("database_table-row")) return () => columns.map(column => Switch.Match(column) - .case("t.database.column_names.name", () => FileNameCell(child)) + .case("t.database.column_names.name", () => FileNameCell(child, is_editable)) .case("t.database.column_names.labels", () => LabelsCell(child.get_fsid())) .case("t.database.column_names.created_at", () => DateCell(child.get_created_at())) .case("t.database.column_names.parent", () => LinksCell(child, "parent")) @@ -79,13 +79,7 @@ const LinksCell = (metadata: Ordo.Metadata.Instance, type: "parent" | "incoming" const parent = get_parent() if (!parent) return - else - return MetadataLink({ - metadata: parent, - href: `/editor/${parent.get_fsid()}`, - children: parent.get_name(), - title: parent.get_name(), - }) + else return MetadataLink({ metadata: parent, children: parent.get_name(), title: parent.get_name() }) } }), ) @@ -94,11 +88,7 @@ const LinksCell = (metadata: Ordo.Metadata.Instance, type: "parent" | "incoming" use(MaokaJabs.set_class("database_cell-multiple")) const get_links = use(MaokaOrdo.Jabs.Metadata.get_outgoing_links$(fsid)) return () => - get_links().map(link => - LinkBlock(() => - MetadataLink({ metadata: link, href: `/editor/${link.get_fsid()}`, children: link.get_name() ?? "/" }), - ), - ) + get_links().map(link => LinkBlock(() => MetadataLink({ metadata: link, children: link.get_name() ?? "/" }))) }), ) .case("incoming", () => @@ -106,11 +96,7 @@ const LinksCell = (metadata: Ordo.Metadata.Instance, type: "parent" | "incoming" use(MaokaJabs.set_class("database_cell-multiple")) const get_links = use(MaokaOrdo.Jabs.Metadata.get_incoming_links$(metadata.get_fsid())) return () => - get_links().map(link => - LinkBlock(() => - MetadataLink({ metadata: link, href: `/editor/${link.get_fsid()}`, children: link.get_name() ?? "/" }), - ), - ) + get_links().map(link => LinkBlock(() => MetadataLink({ metadata: link, children: link.get_name() ?? "/" }))) }), ) .default(noop) @@ -143,7 +129,7 @@ const DateCell = (date: Date) => return () => date.toDateString() }) -const FileNameCell = (metadata: Ordo.Metadata.Instance) => +const FileNameCell = (metadata: Ordo.Metadata.Instance, is_editable: boolean) => Maoka.create("td", ({ use }) => { const { emit } = use(MaokaOrdo.Jabs.get_commands) @@ -168,12 +154,20 @@ const FileNameCell = (metadata: Ordo.Metadata.Instance) => .cata(R.catas.if_ok(new_name => commands.emit("cmd.metadata.rename", { fsid, new_name }))) } - return () => [MetadataIcon({ metadata, custom_class: "pt-0.5" }), EditableLink({ fsid, name, on_blur: handle_blur })] + return () => [ + MetadataIcon({ metadata, custom_class: "pt-0.5" }), + EditableLink({ fsid, name, on_blur: handle_blur, is_editable }), + ] }) }) -type TEditableLinkParams = { name: string; on_blur: (event: FocusEvent) => void; fsid: Ordo.Metadata.FSID } -const EditableLink = ({ name, on_blur, fsid }: TEditableLinkParams) => +type TEditableLinkParams = { + name: string + is_editable: boolean + on_blur: (event: FocusEvent) => void + fsid: Ordo.Metadata.FSID +} +const EditableLink = ({ name, on_blur, is_editable, fsid }: TEditableLinkParams) => Maoka.create("div", ({ use, element }) => { const handle_keydown = (event: KeyboardEvent) => { if (event.key !== "Enter" && event.key !== "Escape") return @@ -182,10 +176,10 @@ const EditableLink = ({ name, on_blur, fsid }: TEditableLinkParams) => if (element instanceof HTMLElement) element.blur() } - use(MaokaJabs.set_attribute("contenteditable", "true")) use(MaokaJabs.set_class("database_cell-filename-text")) use(MaokaJabs.listen("onkeydown", handle_keydown)) use(MaokaJabs.listen("onblur", on_blur)) + if (is_editable) use(MaokaJabs.set_attribute("contenteditable", "true")) return () => Link({ href: `/editor/${fsid}`, children: name }) }) diff --git a/lib/function-database/src/database.component.ts b/lib/function-database/src/database.component.ts index 79638be14..c1f805cf9 100644 --- a/lib/function-database/src/database.component.ts +++ b/lib/function-database/src/database.component.ts @@ -24,7 +24,6 @@ import { MaokaJabs } from "@ordo-pink/maoka-jabs" import { MaokaOrdo } from "@ordo-pink/maoka-ordo-jabs" import { R } from "@ordo-pink/result" import { Switch } from "@ordo-pink/switch" -import { is_string } from "@ordo-pink/tau" import { DatabaseOptions } from "./components/database-options.component" import { DatabaseTableActionsRow } from "./components/database-table-actions-row.component" @@ -37,12 +36,16 @@ import { show_columns_jab } from "./jabs/show-columns-modal.jab" import "./database.css" -export const Database = (metadata: Ordo.Metadata.Instance, content: Ordo.Content.Instance | null, state?: TDatabaseState) => { - const initial_state = state ? state : is_string(content) ? (JSON.parse(content) as TDatabaseState) : {} - database$.replace(initial_state) +export const Database = async (metadata: Ordo.Metadata.Instance, content: Ordo.Content.Instance, is_editable: boolean) => { + try { + const initial_state = content ? ((await new Response(content).json()) as TDatabaseState) : {} + database$.replace(initial_state) + } catch (e) { + database$.replace({}) + } return Maoka.create("div", ({ use, onunmount }) => { - let db_state = initial_state + let db_state = database$.unwrap() const fsid = metadata.get_fsid() use(MaokaJabs.set_class("database_view")) @@ -53,7 +56,8 @@ export const Database = (metadata: Ordo.Metadata.Instance, content: Ordo.Content const metadata_query = use(MaokaOrdo.Jabs.get_metadata_query) use(MaokaOrdo.Jabs.Metadata.get_children_count$(fsid)) - const divorce_database$ = database$.marry(state => { + const divorce_database$ = database$.marry((state, is_update) => { + if (!is_update) return db_state = state commands.emit("cmd.content.set", { fsid, content_type: "database/ordo", content: JSON.stringify(state) }) }) @@ -79,12 +83,12 @@ export const Database = (metadata: Ordo.Metadata.Instance, content: Ordo.Content const sorted_children = to_sorted_children(db_state, children) return [ - DatabaseOptions, + is_editable ? DatabaseOptions : void 0, DatabaseTable(() => [ - DatabaseTableHead(keys), + DatabaseTableHead(keys, is_editable), DatabaseTableBody(() => [ - ...sorted_children.map(child => DatabaseTableRow(keys, child)), - DatabaseTableActionsRow(metadata), + ...sorted_children.map(child => DatabaseTableRow(keys, child, is_editable)), + is_editable ? DatabaseTableActionsRow(metadata) : void 0, ]), ]), ] @@ -159,8 +163,10 @@ const to_sorted_children = (db_state: TDatabaseState, children: Ordo.Metadata.In } const handle_toggle_column_cmd: Ordo.Command.HandlerOf<"cmd.database.toggle_column"> = column => - database$.update_all(({ visible_columns: columns, sorting }) => { - if (!columns) columns = ["t.database.column_names.name", "t.database.column_names.labels"] + database$.update_all(({ visible_columns, sorting }) => { + const columns = visible_columns + ? [...visible_columns] + : (["t.database.column_names.name", "t.database.column_names.labels"] as string[]) if (columns.includes(column)) { columns.splice(columns.indexOf(column), 1) diff --git a/lib/function-database/src/database.css b/lib/function-database/src/database.css index 214e244ef..7ce046a3d 100644 --- a/lib/function-database/src/database.css +++ b/lib/function-database/src/database.css @@ -2,6 +2,10 @@ @apply p-4; } +.pointable { + @apply cursor-pointer; +} + .database_border-color { @apply border-neutral-300 dark:border-neutral-700; } @@ -55,7 +59,7 @@ } .database_table-head_cell { - @apply database_border-color cursor-pointer border px-2 py-1 text-sm font-bold text-neutral-500; + @apply database_border-color border px-2 py-1 text-sm font-bold text-neutral-500; } .database_table-head_cell-content { diff --git a/lib/function-rich-text/index.ts b/lib/function-rich-text/index.ts index aaa6db282..46a57a44c 100644 --- a/lib/function-rich-text/index.ts +++ b/lib/function-rich-text/index.ts @@ -71,7 +71,7 @@ export default create_function( description: "t.text.file_association.description", }, ], - render: ({ metadata, content }) => RichText(metadata, content!), + render: ({ metadata, content }) => RichText(metadata, content), render_icon: BsFileEarmarkRichText, }) }, diff --git a/lib/maoka-components/maoka-components.css b/lib/maoka-components/maoka-components.css new file mode 100644 index 000000000..2209b4fc5 --- /dev/null +++ b/lib/maoka-components/maoka-components.css @@ -0,0 +1,212 @@ +.action-list-item { + @apply w-full cursor-pointer select-none rounded-sm px-2 py-1 hover:bg-gradient-to-br hover:from-neutral-100 hover:to-stone-100 md:px-1 md:py-0.5 hover:dark:bg-gradient-to-br hover:dark:from-neutral-600 hover:dark:to-stone-600; +} + +.action-list-item_layout { + @apply flex flex-col; +} + +.action-list-item_main { + @apply flex items-center space-x-2 px-2; +} + +.action-list-item.active { + @apply bg-gradient-to-br from-purple-400/50 to-pink-400/50 dark:from-pink-900 dark:to-rose-900; +} + +.action-list-item.active-hover { + @apply hover:!bg-gradient-to-br hover:!from-pink-300 hover:!to-rose-300 hover:dark:!from-pink-900 hover:dark:!to-rose-900; +} + +.action-list-item_title { + @apply line-clamp-1 w-full cursor-pointer text-ellipsis; +} + +.button { + @apply flex flex-wrap items-center justify-center space-x-2 rounded-md px-4 py-1 text-sm outline-none transition-all duration-300; + @apply focus:!opacity-100 focus:ring-2 focus:ring-pink-600 focus:dark:ring-pink-400; + @apply active:!opacity-100 active:ring-2 active:ring-pink-600 active:dark:ring-pink-400; + @apply target:!opacity-100 target:ring-2 target:ring-pink-600 target:dark:ring-pink-400; +} + +.button:disabled { + @apply animate-none cursor-not-allowed !bg-neutral-400 ring-0 hover:!scale-100 hover:!bg-neutral-400 dark:!bg-neutral-600 hover:dark:!bg-neutral-600; +} + +.button.neutral { + @apply opacity-50 hover:opacity-100; +} + +.button.success { + @apply bg-emerald-300 shadow-md hover:scale-110 hover:shadow-lg dark:bg-emerald-700; +} + +.button.primary { + @apply bg-neutral-800 text-neutral-200 shadow-md hover:scale-110 hover:bg-pink-600 hover:shadow-lg dark:bg-neutral-200 dark:text-neutral-900 hover:dark:bg-pink-400; +} + +.checkbox { + @apply size-5 shrink-0 cursor-pointer accent-pink-400; +} + +.checkbox-label { + @apply flex w-full items-start gap-x-1; +} + +.checkbox-label > .checkbox { + @apply mt-1 size-4; +} + +.dialog { + @apply flex w-full flex-col gap-y-2 p-4 sm:w-96; +} + +.dialog_header { + @apply flex items-center gap-x-2; +} + +.dialog_title { + @apply text-lg; +} + +.dialog_footer { + @apply flex items-center justify-end gap-x-2; +} + +.hotkey { + @apply hidden shrink-0 items-center justify-center space-x-1 rounded-md px-2 py-0.5 text-center text-xs text-current sm:flex; +} + +.hotkey.mobile { + @apply flex; +} + +.hotkey.smol { + @apply px-1 py-0.5; +} + +.key-container { + @apply min-w-8 rounded-md border border-b-2 border-r-2 border-current px-1 py-0.5 text-current; +} + +.input-wrapper { + @apply relative w-full; +} + +.input_label { + @apply w-full text-sm font-bold; +} + +.input_text { + @apply w-full rounded-md border-0 bg-transparent px-2 py-1 shadow-inner outline-none placeholder:text-neutral-500; +} + +.input_text.non-transparent { + @apply bg-gradient-to-br from-neutral-100 to-stone-100 hover:from-neutral-100 hover:to-stone-100 dark:from-neutral-600 dark:to-stone-600 hover:dark:from-neutral-600 hover:dark:to-stone-600; +} + +.input_text-error { + @apply absolute top-0 w-full text-right text-sm font-bold text-rose-500; +} + +.label { + @apply cursor-pointer rounded-sm px-1 py-0.5 text-xs shadow-sm; + @apply hover:scale-105 hover:transition-all; + @apply flex items-center gap-x-1; +} + +.label_remove { + @apply rounded-lg opacity-0 transition-all hover:bg-neutral-500/50 hover:opacity-100; +} + +.label.default { + @apply bg-neutral-200 dark:bg-neutral-600/75; +} + +.label.red { + @apply bg-red-200 dark:bg-red-600/75; +} + +.label.orange { + @apply bg-orange-200 dark:bg-orange-600/75; +} + +.label.amber { + @apply bg-amber-200 dark:bg-amber-600/75; +} + +.label.yellow { + @apply bg-yellow-200 dark:bg-yellow-600/75; +} + +.label.lime { + @apply bg-lime-200 dark:bg-lime-600/75; +} + +.label.green { + @apply bg-green-200 dark:bg-green-600/75; +} + +.label.emerald { + @apply bg-emerald-200 dark:bg-emerald-600/75; +} + +.label.teal { + @apply bg-teal-200 dark:bg-teal-600/75; +} + +.label.cyan { + @apply bg-cyan-200 dark:bg-cyan-600/75; +} + +.label.sky { + @apply bg-sky-200 dark:bg-sky-600/75; +} + +.label.blue { + @apply bg-blue-200 dark:bg-blue-600/75; +} + +.label.indigo { + @apply bg-indigo-200 dark:bg-indigo-600/75; +} + +.label.violet { + @apply bg-violet-200 dark:bg-violet-600/75; +} + +.label.purple { + @apply bg-purple-200 dark:bg-purple-600/75; +} + +.label.fuchsia { + @apply bg-fuchsia-200 dark:bg-fuchsia-600/75; +} + +.label.pink { + @apply bg-pink-200 dark:bg-pink-600/75; +} + +.label.rose { + @apply bg-rose-200 dark:bg-rose-600/75; +} + +.select_color-picker_options-wrapper { + @apply absolute left-0 right-0 top-7 max-h-48 overflow-auto rounded-sm bg-neutral-50 shadow-lg backdrop-blur-md dark:bg-neutral-800/90; +} + +.link { + @apply cursor-pointer rounded-sm underline decoration-neutral-500/50 decoration-1 underline-offset-2 transition-all hover:text-rose-600 hover:decoration-rose-500/50 dark:hover:text-rose-300; +} + +.link_no-history { + @apply text-inherit visited:text-inherit; +} + +.link_wrapper { + @apply flex w-fit items-center gap-x-1; +} + +.link_text-wrapper { + @apply line-clamp-1 text-ellipsis; +} diff --git a/lib/maoka-components/src/common/action-list-item.component.ts b/lib/maoka-components/src/common/action-list-item.component.ts index 3af2f57a0..5244287f0 100644 --- a/lib/maoka-components/src/common/action-list-item.component.ts +++ b/lib/maoka-components/src/common/action-list-item.component.ts @@ -22,7 +22,7 @@ import { Maoka, type TMaokaChildren } from "@ordo-pink/maoka" import { MaokaJabs } from "@ordo-pink/maoka-jabs" -import "./action-list-item.css" +import "../../maoka-components.css" export type TActionListItemProps = { title: string diff --git a/lib/maoka-components/src/common/action-list-item.css b/lib/maoka-components/src/common/action-list-item.css deleted file mode 100644 index 14e4f6bff..000000000 --- a/lib/maoka-components/src/common/action-list-item.css +++ /dev/null @@ -1,23 +0,0 @@ -.action-list-item { - @apply w-full cursor-pointer select-none rounded-sm px-2 py-1 hover:bg-gradient-to-br hover:from-neutral-100 hover:to-stone-100 md:px-1 md:py-0.5 hover:dark:bg-gradient-to-br hover:dark:from-neutral-600 hover:dark:to-stone-600; -} - -.action-list-item_layout { - @apply flex flex-col; -} - -.action-list-item_main { - @apply flex items-center space-x-2 px-2; -} - -.action-list-item.active { - @apply bg-gradient-to-br from-purple-400/50 to-pink-400/50 dark:from-pink-900 dark:to-rose-900; -} - -.action-list-item.active-hover { - @apply hover:!bg-gradient-to-br hover:!from-pink-300 hover:!to-rose-300 hover:dark:!from-pink-900 hover:dark:!to-rose-900; -} - -.action-list-item_title { - @apply line-clamp-1 w-full cursor-pointer text-ellipsis; -} diff --git a/lib/maoka-components/src/common/button.component.ts b/lib/maoka-components/src/common/button.component.ts index 261276117..ff93d04f6 100644 --- a/lib/maoka-components/src/common/button.component.ts +++ b/lib/maoka-components/src/common/button.component.ts @@ -24,7 +24,7 @@ import { MaokaJabs } from "@ordo-pink/maoka-jabs" import { Hotkey, THotkeyOptions } from "./hotkey.component" -import "./button.css" +import "../../maoka-components.css" export type TButtonProps = { on_click: (event: MouseEvent) => void | Promise diff --git a/lib/maoka-components/src/common/button.css b/lib/maoka-components/src/common/button.css deleted file mode 100644 index e64c38ccd..000000000 --- a/lib/maoka-components/src/common/button.css +++ /dev/null @@ -1,22 +0,0 @@ -.button { - @apply flex flex-wrap items-center justify-center space-x-2 rounded-md px-4 py-1 text-sm outline-none transition-all duration-300; - @apply focus:!opacity-100 focus:ring-2 focus:ring-pink-600 focus:dark:ring-pink-400; - @apply active:!opacity-100 active:ring-2 active:ring-pink-600 active:dark:ring-pink-400; - @apply target:!opacity-100 target:ring-2 target:ring-pink-600 target:dark:ring-pink-400; -} - -.button:disabled { - @apply animate-none cursor-not-allowed !bg-neutral-400 ring-0 hover:!scale-100 hover:!bg-neutral-400 dark:!bg-neutral-600 hover:dark:!bg-neutral-600; -} - -.button.neutral { - @apply opacity-50 hover:opacity-100; -} - -.button.success { - @apply bg-emerald-300 shadow-md hover:scale-110 hover:shadow-lg dark:bg-emerald-700; -} - -.button.primary { - @apply bg-neutral-800 text-neutral-200 shadow-md hover:scale-110 hover:bg-pink-600 hover:shadow-lg dark:bg-neutral-200 dark:text-neutral-900 hover:dark:bg-pink-400; -} diff --git a/lib/maoka-components/src/common/checkbox.component.ts b/lib/maoka-components/src/common/checkbox.component.ts index 8d0e73a10..640f5a757 100644 --- a/lib/maoka-components/src/common/checkbox.component.ts +++ b/lib/maoka-components/src/common/checkbox.component.ts @@ -22,7 +22,7 @@ import { Maoka } from "@ordo-pink/maoka" import { MaokaJabs } from "@ordo-pink/maoka-jabs" -import "./checkbox.css" +import "../../maoka-components.css" export type TCheckboxParams = { on_change: (event: Event) => void diff --git a/lib/maoka-components/src/common/checkbox.css b/lib/maoka-components/src/common/checkbox.css deleted file mode 100644 index bad1a0d66..000000000 --- a/lib/maoka-components/src/common/checkbox.css +++ /dev/null @@ -1,11 +0,0 @@ -.checkbox { - @apply size-5 shrink-0 cursor-pointer accent-pink-400; -} - -.checkbox-label { - @apply flex w-full items-start gap-x-1; -} - -.checkbox-label > .checkbox { - @apply mt-1 size-4; -} diff --git a/lib/maoka-components/src/common/dialog.component.ts b/lib/maoka-components/src/common/dialog.component.ts index 7eacf6bd1..69d974f25 100644 --- a/lib/maoka-components/src/common/dialog.component.ts +++ b/lib/maoka-components/src/common/dialog.component.ts @@ -23,7 +23,7 @@ import { Maoka, type TMaokaChildren } from "@ordo-pink/maoka" import { Button } from "@ordo-pink/maoka-components" import { MaokaJabs } from "@ordo-pink/maoka-jabs" -import "./dialog.css" +import "../../maoka-components.css" type TDialogParams = { title: string diff --git a/lib/maoka-components/src/common/dialog.css b/lib/maoka-components/src/common/dialog.css deleted file mode 100644 index e1a44c673..000000000 --- a/lib/maoka-components/src/common/dialog.css +++ /dev/null @@ -1,15 +0,0 @@ -.dialog { - @apply flex w-full flex-col gap-y-2 p-4 sm:w-96; -} - -.dialog_header { - @apply flex items-center gap-x-2; -} - -.dialog_title { - @apply text-lg; -} - -.dialog_footer { - @apply flex items-center justify-end gap-x-2; -} diff --git a/lib/maoka-components/src/common/hotkey.component.ts b/lib/maoka-components/src/common/hotkey.component.ts index a9a02e942..de2002f9e 100644 --- a/lib/maoka-components/src/common/hotkey.component.ts +++ b/lib/maoka-components/src/common/hotkey.component.ts @@ -26,7 +26,7 @@ import { Switch } from "@ordo-pink/switch" import { create_hotkey_from_event } from "@ordo-pink/hotkey-from-event" import { title_case } from "@ordo-pink/tau" -import "./hotkey.css" +import "../../maoka-components.css" export type THotkeyOptions = { prevent_in_inputs?: boolean diff --git a/lib/maoka-components/src/common/hotkey.css b/lib/maoka-components/src/common/hotkey.css deleted file mode 100644 index a53e84040..000000000 --- a/lib/maoka-components/src/common/hotkey.css +++ /dev/null @@ -1,15 +0,0 @@ -.hotkey { - @apply hidden shrink-0 items-center justify-center space-x-1 rounded-md px-2 py-0.5 text-center text-xs text-current sm:flex; -} - -.hotkey.mobile { - @apply flex; -} - -.hotkey.smol { - @apply px-1 py-0.5; -} - -.key-container { - @apply min-w-8 rounded-md border border-b-2 border-r-2 border-current px-1 py-0.5 text-current; -} diff --git a/lib/maoka-components/src/common/input.component.ts b/lib/maoka-components/src/common/input.component.ts index ea4ac9dbf..079a246c3 100644 --- a/lib/maoka-components/src/common/input.component.ts +++ b/lib/maoka-components/src/common/input.component.ts @@ -27,7 +27,7 @@ import { ZAGS } from "@ordo-pink/zags" const is_valid$ = ZAGS.Of({ value: true }) -import "./input.css" +import "../../maoka-components.css" type TInputProps = { initial_value?: string diff --git a/lib/maoka-components/src/common/input.css b/lib/maoka-components/src/common/input.css deleted file mode 100644 index 126784f5a..000000000 --- a/lib/maoka-components/src/common/input.css +++ /dev/null @@ -1,19 +0,0 @@ -.input-wrapper { - @apply relative w-full; -} - -.input_label { - @apply w-full text-sm font-bold; -} - -.input_text { - @apply w-full rounded-md border-0 bg-transparent px-2 py-1 shadow-inner outline-none placeholder:text-neutral-500; -} - -.input_text.non-transparent { - @apply bg-gradient-to-br from-neutral-100 to-stone-100 hover:from-neutral-100 hover:to-stone-100 dark:from-neutral-600 dark:to-stone-600 hover:dark:from-neutral-600 hover:dark:to-stone-600; -} - -.input_text-error { - @apply absolute top-0 w-full text-right text-sm font-bold text-rose-500; -} diff --git a/lib/maoka-components/src/common/label.component.ts b/lib/maoka-components/src/common/label.component.ts index 5a95506a4..20aa4a9b2 100644 --- a/lib/maoka-components/src/common/label.component.ts +++ b/lib/maoka-components/src/common/label.component.ts @@ -25,7 +25,7 @@ import { Maoka } from "@ordo-pink/maoka" import { MaokaJabs } from "@ordo-pink/maoka-jabs" import { is_string } from "@ordo-pink/tau" -import "./label.css" +import "../../maoka-components.css" export const Label = (label: Ordo.Metadata.Label, emit: Ordo.Command.EmitFn, metadata?: Ordo.Metadata.Instance) => Maoka.create("div", ({ use }) => { diff --git a/lib/maoka-components/src/common/label.css b/lib/maoka-components/src/common/label.css deleted file mode 100644 index 9dc21a6f7..000000000 --- a/lib/maoka-components/src/common/label.css +++ /dev/null @@ -1,81 +0,0 @@ -.label { - @apply cursor-pointer rounded-sm px-1 py-0.5 text-xs shadow-sm; - @apply hover:scale-105 hover:transition-all; - @apply flex items-center gap-x-1; -} - -.label_remove { - @apply rounded-lg opacity-0 transition-all hover:bg-neutral-500/50 hover:opacity-100; -} - -.label.default { - @apply bg-neutral-200 dark:bg-neutral-600/75; -} - -.label.red { - @apply bg-red-200 dark:bg-red-600/75; -} - -.label.orange { - @apply bg-orange-200 dark:bg-orange-600/75; -} - -.label.amber { - @apply bg-amber-200 dark:bg-amber-600/75; -} - -.label.yellow { - @apply bg-yellow-200 dark:bg-yellow-600/75; -} - -.label.lime { - @apply bg-lime-200 dark:bg-lime-600/75; -} - -.label.green { - @apply bg-green-200 dark:bg-green-600/75; -} - -.label.emerald { - @apply bg-emerald-200 dark:bg-emerald-600/75; -} - -.label.teal { - @apply bg-teal-200 dark:bg-teal-600/75; -} - -.label.cyan { - @apply bg-cyan-200 dark:bg-cyan-600/75; -} - -.label.sky { - @apply bg-sky-200 dark:bg-sky-600/75; -} - -.label.blue { - @apply bg-blue-200 dark:bg-blue-600/75; -} - -.label.indigo { - @apply bg-indigo-200 dark:bg-indigo-600/75; -} - -.label.violet { - @apply bg-violet-200 dark:bg-violet-600/75; -} - -.label.purple { - @apply bg-purple-200 dark:bg-purple-600/75; -} - -.label.fuchsia { - @apply bg-fuchsia-200 dark:bg-fuchsia-600/75; -} - -.label.pink { - @apply bg-pink-200 dark:bg-pink-600/75; -} - -.label.rose { - @apply bg-rose-200 dark:bg-rose-600/75; -} diff --git a/lib/maoka-components/src/common/link.component.ts b/lib/maoka-components/src/common/link.component.ts index 8a8a62e9a..751b6b108 100644 --- a/lib/maoka-components/src/common/link.component.ts +++ b/lib/maoka-components/src/common/link.component.ts @@ -26,7 +26,11 @@ import { is_string } from "@ordo-pink/tau" import { MetadataIcon } from "../metadata/metadata-icon.component" -type P = { href: `/${string}`; children?: TMaokaChildren; custom_class?: string; show_visited?: boolean; title?: string } +import "../../maoka-components.css" +import { MaokaStr } from "@ordo-pink/maoka-render-string" +import { ordo_app_state } from "@ordo-pink/frontend-app/app.state" + +type P = { href: string; children?: TMaokaChildren; custom_class?: string; show_visited?: boolean; title?: string } export const Link = ({ href, children, custom_class, show_visited, title }: P) => Maoka.create("a", ({ use }) => { const { emit } = use(MaokaOrdo.Jabs.get_commands) @@ -34,48 +38,57 @@ export const Link = ({ href, children, custom_class, show_visited, title }: P) = use(MaokaJabs.listen("onclick", click_listener(emit, href))) use(MaokaJabs.set_attribute("href", href)) use(MaokaJabs.set_attribute("title", title)) - use(MaokaJabs.set_class(default_class)) + use(MaokaJabs.set_class("link")) if (custom_class) use(MaokaJabs.add_class(custom_class)) - if (!show_visited) use(MaokaJabs.add_class(ignore_history_highlighting_class)) + if (!show_visited) use(MaokaJabs.add_class("link_no-history")) if (is_string(children)) use(MaokaJabs.set_attribute("title", children)) return () => children }) export const MetadataLink = ({ - href, children, custom_class, show_visited, metadata, title, -}: P & { metadata: Ordo.Metadata.Instance }) => - Link({ - children: MetadataLinkWrapper(() => [ - MetadataIcon({ metadata, show_emoji_picker: false }), - MetadataLinkTextWrapper(() => children), - ]), - custom_class: `no-underline ${custom_class}`, - href, - show_visited, - title, +}: Omit & { metadata: Ordo.Metadata.Instance }) => + Maoka.create("span", ({ element }) => { + let href = `/editor/${metadata.get_fsid()}` + + const user_query = ordo_app_state.zags.select("auth.user") + const pb_host = ordo_app_state.zags.select("hosts.pb") + + if (user_query && MaokaStr.is_maoka_str_element(element)) { + const name = metadata.get_property("public_name") + href = `${pb_host}/${user_query.get_handle()}/${name}` + } + + return () => + Link({ + children: MetadataLinkWrapper(() => [ + MetadataIcon({ metadata, show_emoji_picker: false }), + MetadataLinkTextWrapper(() => children), + ]), + custom_class: `no-underline ${custom_class}`, + href, + show_visited, + title, + }) }) // --- Internal --- -const default_class = - "underline transition-all hover:text-rose-600 dark:hover:text-rose-300 rounded-sm cursor-pointer decoration-neutral-500/50 hover:decoration-rose-500/50 decoration-1 underline-offset-2" - -const MetadataLinkWrapper = Maoka.styled("div", { class: "w-fit flex items-center gap-x-1" }) +const MetadataLinkWrapper = Maoka.styled("div", { class: "link_wrapper" }) -const MetadataLinkTextWrapper = Maoka.styled("div", { class: `${default_class} text-ellipsis line-clamp-1` }) +const MetadataLinkTextWrapper = Maoka.styled("div", { class: "link link_text-wrapper" }) -const click_listener = (emit: Ordo.Command.Commands["emit"], url: `/${string}`) => (event: MouseEvent) => { +const click_listener = (emit: Ordo.Command.Commands["emit"], url: string) => (event: MouseEvent) => { event.preventDefault() event.stopPropagation() - emit("cmd.application.router.navigate", { url }) + url.startsWith("/") + ? emit("cmd.application.router.navigate", { url: url as `/${string}` }) + : emit("cmd.application.router.open_external", { url, new_tab: true }) } - -const ignore_history_highlighting_class = "text-inherit visited:text-inherit" diff --git a/lib/maoka-components/src/common/select.component.ts b/lib/maoka-components/src/common/select.component.ts index ac26f5385..a00de2182 100644 --- a/lib/maoka-components/src/common/select.component.ts +++ b/lib/maoka-components/src/common/select.component.ts @@ -20,12 +20,12 @@ */ import { Maoka, type TMaokaChildren } from "@ordo-pink/maoka" +import { BsChevronDown } from "@ordo-pink/frontend-icons" import { MaokaJabs } from "@ordo-pink/maoka-jabs" import { ActionListItem } from "./action-list-item.component" -import "./select.css" -import { BsChevronDown } from "@ordo-pink/frontend-icons" +import "../../maoka-components.css" export type TSelectOption<$TValue> = { title: string diff --git a/lib/maoka-components/src/common/select.css b/lib/maoka-components/src/common/select.css deleted file mode 100644 index 48a4845a6..000000000 --- a/lib/maoka-components/src/common/select.css +++ /dev/null @@ -1,3 +0,0 @@ -.select_color-picker_options-wrapper { - @apply absolute left-0 right-0 top-7 max-h-48 overflow-auto rounded-sm bg-neutral-50 shadow-lg backdrop-blur-md dark:bg-neutral-800/90; -} diff --git a/lib/maoka-ordo-jabs/index.ts b/lib/maoka-ordo-jabs/index.ts index 66a0a1396..1ad69e39a 100644 --- a/lib/maoka-ordo-jabs/index.ts +++ b/lib/maoka-ordo-jabs/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Unlicense */ -import { Maoka, type TMaokaChildren } from "@ordo-pink/maoka" +import { Maoka, type TMaokaComponent } from "@ordo-pink/maoka" import { get_commands, @@ -52,12 +52,12 @@ export const MaokaOrdo = { }, Context: ordo_context, Components: { - WithState: (ctx: Ordo.CreateFunction.State, children: () => TMaokaChildren) => + WithState: (ctx: Ordo.CreateFunction.State, children: () => TMaokaComponent | Promise) => Maoka.create("div", ({ use }) => { use(MaokaOrdo.Context.provide(ctx)) - return children + return async () => children() }), - WithStateCurry: (ctx: Ordo.CreateFunction.State) => (children: () => TMaokaChildren) => + WithStateCurry: (ctx: Ordo.CreateFunction.State) => (children: () => TMaokaComponent | Promise) => MaokaOrdo.Components.WithState(ctx, children), }, } diff --git a/lib/maoka-render-string/index.ts b/lib/maoka-render-string/index.ts new file mode 100644 index 000000000..10d01c24b --- /dev/null +++ b/lib/maoka-render-string/index.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { create_element, is_maoka_str_element, render } from "./src/maoka-render-string.impl" + +export * from "./src/maoka-render-string.types" + +export const MaokaStr = { create_element, is_maoka_str_element, render } diff --git a/lib/maoka-render-string/license b/lib/maoka-render-string/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/maoka-render-string/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/maoka-render-string/readme.md b/lib/maoka-render-string/readme.md new file mode 100644 index 000000000..bcf67a028 --- /dev/null +++ b/lib/maoka-render-string/readme.md @@ -0,0 +1 @@ +# Maoka Render String diff --git a/lib/maoka-render-string/src/maoka-render-string.impl.ts b/lib/maoka-render-string/src/maoka-render-string.impl.ts new file mode 100644 index 000000000..7ec5c9d8d --- /dev/null +++ b/lib/maoka-render-string/src/maoka-render-string.impl.ts @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { type TMaokaChild } from "@ordo-pink/maoka" + +import { type TMaokaRenderStringFn, type TMaokaStrElement } from "./maoka-render-string.types" + +export const render: TMaokaRenderStringFn = async (root, component) => { + const root_id = crypto.randomUUID() + const Component = await component(create_element, root, root_id) + + root.appendChild(Component) + + return root.str() +} + +export const create_element = (tag: string): TMaokaStrElement => { + const attributes = {} as Record + let children = [] as TMaokaChild[] + + return { + setAttribute: (qualified_name: string, value: string) => { + attributes[qualified_name] = value + }, + getAttribute: (qualified_name: string) => qualified_name, + removeAttribute: (qualified_name: string) => { + delete attributes[qualified_name] + }, + appendChild: child => { + children.push(child) + return child + }, + replaceChildren: (...new_children) => { + children = new_children + }, + get children() { + return children + }, + dispatchEvent: () => false, + str: async (depth = 0) => { + const result = "<" + .concat(tag) + .concat(Object.keys(attributes).length ? " " : "") + .concat(Object.keys(attributes).reduce((acc, key) => acc.concat(`${key}="${attributes[key]}" `), "")) + .concat(">") + + const child_strings = [] as string[] + + for (const child of children) { + if (!child) { + continue + } else if (is_maoka_str_element(child)) { + child_strings.push(await child.str(depth + 1)) + } else if (typeof child === "string") { + child_strings.push(child) + } else if (typeof child === "number") { + child_strings.push(String(child)) + } + } + + return result.concat(child_strings.join("")).concat("") + }, + } +} + +export const is_maoka_str_element = (x: any): x is TMaokaStrElement => + !!x && typeof x === "object" && x.str && typeof x.str === "function" diff --git a/lib/maoka-render-string/src/maoka-render-string.test.ts b/lib/maoka-render-string/src/maoka-render-string.test.ts new file mode 100644 index 000000000..a4aa56c14 --- /dev/null +++ b/lib/maoka-render-string/src/maoka-render-string.test.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { expect, test } from "bun:test" +import { maoka_render_string } from "./maoka-render-string.impl" + +test("maoka-render-string should pass", () => { + expect(maoka_render_string).toEqual("maoka-render-string") +}) diff --git a/lib/maoka-render-string/src/maoka-render-string.types.ts b/lib/maoka-render-string/src/maoka-render-string.types.ts new file mode 100644 index 000000000..c4c4d9b4b --- /dev/null +++ b/lib/maoka-render-string/src/maoka-render-string.types.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import type { TMaokaComponent, TMaokaElement } from "@ordo-pink/maoka" + +export type TMaokaRenderStringFn = (root: TMaokaStrElement, component: TMaokaComponent) => Promise +export type TMaokaStrElement = TMaokaElement & { str: (depth?: number) => Promise } diff --git a/lib/maoka/src/maoka.impl.ts b/lib/maoka/src/maoka.impl.ts index e9f3c35fb..f40ae54aa 100644 --- a/lib/maoka/src/maoka.impl.ts +++ b/lib/maoka/src/maoka.impl.ts @@ -75,7 +75,8 @@ export const styled = export const html = (tag: string, html: string): T.TMaokaComponent => create(tag, ({ element }) => { - element.innerHTML = html + if (element instanceof Element) return void (element.innerHTML = html) + return () => html }) export const dom: T.TMaokaRenderDOMFn = async (root, component) => { @@ -86,22 +87,27 @@ export const dom: T.TMaokaRenderDOMFn = async (root, component) => { const Component = await component(create_element, root_element, root_id) const refresh_queue = new Map Promise }>() - root.appendChild(Component as HTMLElement) + root.appendChild(Component as unknown as HTMLElement) root.addEventListener("refresh", event => { event.stopPropagation() - const [id, element, get_children] = (event as any).detail as [string, T.TMaokaElement, () => T.TMaokaComponent] + const [id, element, get_children] = (event as any).detail as [string, T.TMaokaDOMElement, () => T.TMaokaComponent] - const refresh_elements = refresh_queue.keys().toArray() + const refresh_nodes = refresh_queue.keys().toArray() if (refresh_queue.has(id)) return - for (let i = 0; i < refresh_elements.length; i++) { - const e = refresh_queue.get(refresh_elements[i])?.element as HTMLElement + for (let i = 0; i < refresh_nodes.length; i++) { + const refresh_element = refresh_queue.get(refresh_nodes[i])?.element - if (e && element.contains?.(e)) { - refresh_queue.delete(refresh_elements[i]) + if ( + refresh_element && + refresh_element instanceof HTMLElement && + element instanceof HTMLElement && + element.contains?.(refresh_element) + ) { + refresh_queue.delete(refresh_nodes[i]) break } } @@ -124,30 +130,22 @@ export const dom: T.TMaokaRenderDOMFn = async (root, component) => { ).then(() => request_idle_callback(() => void render_loop())) : request_idle_callback(() => void render_loop()) - // const render_loop = () => { - // const next = refresh_queue.entries().next() - - // if (next.value) { - // console.log(next.value[0]) - // refresh_queue.delete(next.value[0]) - // return void next.value[1]().then(() => request_idle_callback(render_loop)) - // } - - // request_idle_callback(render_loop) - // } - request_idle_callback(() => void render_loop()) const unmount_element = (element: T.TMaokaElement) => { if (element.onunmount) element.onunmount() - element.childNodes.forEach(child => unmount_element(child as any)) + for (let i = 0; i < element.children.length; i++) { + unmount_element(element.children[i] as T.TMaokaElement) + } } const mount_element = (element: T.TMaokaElement) => { if (element.onmount) element.onmount() - element.childNodes.forEach(child => mount_element(child as any)) + for (let i = 0; i < element.children.length; i++) { + mount_element(element.children[i] as T.TMaokaElement) + } } mount_element(Component) @@ -169,11 +167,7 @@ export const dom: T.TMaokaRenderDOMFn = async (root, component) => { } }) - observer.observe(root as HTMLElement, { - childList: true, - subtree: true, - attributeFilter: ["onmount", "onunmount"], - }) + observer.observe(root, { childList: true, subtree: true, attributeFilter: ["onmount", "onunmount"] }) } // --- Internal --- @@ -186,7 +180,7 @@ const render_children = async ( element: T.TMaokaElement, ) => { if (!get_children) return element - element.innerHTML = "" + if (element instanceof HTMLElement) element.innerHTML = "" let children = await get_children() if (!children) return element diff --git a/lib/maoka/src/maoka.types.ts b/lib/maoka/src/maoka.types.ts index fbf3bf17a..eabaec70b 100644 --- a/lib/maoka/src/maoka.types.ts +++ b/lib/maoka/src/maoka.types.ts @@ -5,28 +5,31 @@ // TODO: Comments // TODO: Full types -export type TMaokaElement = { [$TKey in keyof HTMLElement]: HTMLElement[$TKey] | undefined } & { - setAttribute: (qualified_name: string, value: string) => void - getAttribute: (qualified_name: string) => string +export type TMaokaElement = { + setAttribute: (qualifiedName: string, value: string) => void + getAttribute: (qualifiedName: string) => string + removeAttribute: (qualifiedName: string) => void appendChild: (child: TMaokaChild) => TMaokaChild replaceChildren: (...children: TMaokaChild[]) => void - childNodes: HTMLElement["childNodes"] dispatchEvent: (event: Event) => void - onunmount: (() => void) | undefined - onmount: (() => void) | undefined + children: TMaokaChild[] + + // TODO Move to render_dom + onunmount?: (() => void) | undefined + onmount?: (() => void) | undefined } export type TMaokaTextElement = Partial<{ [$TKey in keyof Text]: Text[$TKey] }> | string -export type TMaokaCreateMaokaElementFn<$TElement extends TMaokaElement = TMaokaElement> = (name: string) => $TElement +export type TMaokaCreateMaokaElementFn = (name: string) => TMaokaElement export type TMaokaCreateComponentFn = (name: string, callback: TMaokaCallback) => TMaokaComponent -export type TMaokaComponent<$TElement extends TMaokaElement = TMaokaElement> = { - (create_element: TMaokaCreateMaokaElementFn<$TElement>, root_element: TMaokaElement, root_id: string): Promise +export type TMaokaComponent = { + (create_element: TMaokaCreateMaokaElementFn, root_element: TMaokaElement, root_id: string): Promise id?: string rid?: string - element?: $TElement + element?: TMaokaElement refresh?: () => void } @@ -51,7 +54,7 @@ export type TMaokaChildren = TMaokaChild | TMaokaChild[] /** * A record of jabs that are provided by Maoka directly. */ -export type TMaokaProps<$TElement extends TMaokaElement = TMaokaElement> = { +export type TMaokaProps = { /** * Get UUID of current Maoka component. This would probably only be useful for creating custom * jabs that accumulate a set of components to apply batch refresh calls. You would hardly ever @@ -62,7 +65,7 @@ export type TMaokaProps<$TElement extends TMaokaElement = TMaokaElement> = { /** * Returns reference to the current element. */ - get element(): $TElement + get element(): TMaokaElement /** * Root id. @@ -104,7 +107,9 @@ export type TMaokaCallback = ( | Promise<(() => TMaokaChildren) | undefined | void> | Promise<(() => Promise) | undefined | void> -export type TMaokaRenderDOMFn = <$TElement extends HTMLElement = HTMLElement>( - root: $TElement, - component: TMaokaComponent, -) => Promise +export type TMaokaDOMElement = TMaokaElement & { + onunmount: (() => void) | undefined + onmount: (() => void) | undefined +} + +export type TMaokaRenderDOMFn = (root: HTMLElement, component: TMaokaComponent) => Promise diff --git a/lib/oath-indexeddb/index.ts b/lib/oath-indexeddb/index.ts new file mode 100644 index 000000000..607d1c249 --- /dev/null +++ b/lib/oath-indexeddb/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export * from "./src/oath-indexeddb.impl" +export * from "./src/oath-indexeddb.types" diff --git a/lib/oath-indexeddb/license b/lib/oath-indexeddb/license new file mode 100644 index 000000000..f43bdf2be --- /dev/null +++ b/lib/oath-indexeddb/license @@ -0,0 +1,19 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in +source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any +and all copyright interest in the software to the public domain. We make this dedication for the +benefit of the public at large and to the detriment of our heirs and successors. We intend this +dedication to be an overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/oath-indexeddb/readme.md b/lib/oath-indexeddb/readme.md new file mode 100644 index 000000000..7a6acca60 --- /dev/null +++ b/lib/oath-indexeddb/readme.md @@ -0,0 +1 @@ +# Oath Indexeddb diff --git a/lib/oath-indexeddb/src/oath-indexeddb.impl.ts b/lib/oath-indexeddb/src/oath-indexeddb.impl.ts new file mode 100644 index 000000000..972ec2e55 --- /dev/null +++ b/lib/oath-indexeddb/src/oath-indexeddb.impl.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import type { TIndexedDBObjectStorePromiseStatic } from "./oath-indexeddb.types" + +export const IndexedDBStorePromise: TIndexedDBObjectStorePromiseStatic = { + Of: store => ({ + add: (value, key) => promisify_idb_request(store.add(value, key)), + clear: () => promisify_idb_request(store.clear()), + count: query => promisify_idb_request(store.count(query)), + delete: query => promisify_idb_request(store.delete(query)), + get: query => promisify_idb_request(store.get(query)), + get_all: (query, count) => promisify_idb_request(store.getAll(query, count)), + get_all_keys: (query, count) => promisify_idb_request(store.getAllKeys(query, count)), + get_key: query => promisify_idb_request(store.getKey(query)), + put: (value, key) => promisify_idb_request(store.put(value, key)), + }), +} + +const promisify_idb_request = (req: IDBRequest) => + new Promise((resolve, reject) => { + if (req.transaction) { + req.transaction.oncomplete = () => resolve(req.result) + req.transaction.onerror = () => reject(req.error) + req.transaction.onabort = () => reject(new Error("Transaction aborted")) + + return + } + + req.onsuccess = () => resolve(req.result) + req.onerror = () => reject(req.error) + }) diff --git a/lib/oath-indexeddb/src/oath-indexeddb.test.ts b/lib/oath-indexeddb/src/oath-indexeddb.test.ts new file mode 100644 index 000000000..caf855a58 --- /dev/null +++ b/lib/oath-indexeddb/src/oath-indexeddb.test.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +import { expect, test } from "bun:test" +import { oath_indexeddb } from "./oath-indexeddb.impl" + +test("oath-indexeddb should pass", () => { + expect(oath_indexeddb).toEqual("oath-indexeddb") +}) diff --git a/lib/oath-indexeddb/src/oath-indexeddb.types.ts b/lib/oath-indexeddb/src/oath-indexeddb.types.ts new file mode 100644 index 000000000..a9fba5cca --- /dev/null +++ b/lib/oath-indexeddb/src/oath-indexeddb.types.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright 2025, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: Unlicense + */ + +export type IDBCursorWithValue = IDBCursor & { value: T } + +export type TIndexedDBObjectStorePromise = { + add: (value: T, key: IDBValidKey) => Promise + clear: () => Promise + count: (query?: IDBValidKey | IDBKeyRange) => Promise + delete: (query: IDBValidKey | IDBKeyRange) => Promise + get: (query: IDBValidKey | IDBKeyRange) => Promise + get_all: (query?: IDBValidKey | IDBKeyRange | null, count?: number) => Promise + get_all_keys: (query?: IDBValidKey | IDBKeyRange | null, count?: number) => Promise + get_key: (query: IDBValidKey | IDBKeyRange) => Promise + put: (value: T, key?: IDBValidKey) => Promise + // create_index: (name: string, keyPath: string | string[], options?: IDBIndexParameters) => Promise + // delete_index: (name: string) => void + // index: (name: string) => IDBIndex + // open_cursor: ( + // query?: IDBValidKey | IDBKeyRange | null, + // direction?: IDBCursorDirection, + // ) => Promise | null> + // open_key_cursor: (query?: IDBValidKey | IDBKeyRange | null, direction?: IDBCursorDirection) => Promise +} + +export type TIndexedDBObjectStorePromiseStatic = { + Of: (store: IDBObjectStore) => TIndexedDBObjectStorePromise +} diff --git a/lib/oath/src/oath.impl.ts b/lib/oath/src/oath.impl.ts index cab92c3ba..cde95f1b3 100644 --- a/lib/oath/src/oath.impl.ts +++ b/lib/oath/src/oath.impl.ts @@ -240,7 +240,7 @@ export class Oath<$TResolve, $TReject = never> { * * @optional */ - on_error = (error: unknown) => (error instanceof Error ? error : (new Error(String(error as any)) as any)), + on_error: (error: Error) => $TReject = (error: Error) => error as any, /** * Optional abort controller for cases when Oath was cancelled when it already @@ -250,12 +250,14 @@ export class Oath<$TResolve, $TReject = never> { */ abort_controller: AbortController = new AbortController(), ): Oath, $TReject> => { + const to_error = (error: any) => (error instanceof Error ? error : new Error(String(error))) + return new Oath(async (resolve, reject) => { try { // eslint-disable-next-line @typescript-eslint/await-thenable resolve(await thunk()) } catch (error) { - reject(on_error(error)) + reject(on_error(to_error(error))) } }, abort_controller) } diff --git a/lib/oath/src/oath.types.ts b/lib/oath/src/oath.types.ts index be28fdc9f..ca7287d59 100644 --- a/lib/oath/src/oath.types.ts +++ b/lib/oath/src/oath.types.ts @@ -25,7 +25,7 @@ export type TUnderOathRejected = T extends object & { fix(on_reject: infer F): any } ? F extends (value: infer V) => any - ? TUnderOathRejected + ? V : never : never diff --git a/lib/routary-cors/src/routary-cors.impl.ts b/lib/routary-cors/src/routary-cors.impl.ts index 7f0e914b1..626cc61db 100644 --- a/lib/routary-cors/src/routary-cors.impl.ts +++ b/lib/routary-cors/src/routary-cors.impl.ts @@ -15,7 +15,7 @@ export const routary_cors: TRoutaryCORS = Object.keys(shaft).forEach(bearing => { if (bearing === "OPTIONS") return - Object.keys(shaft[bearing as TBearing] as Record>>).forEach(gasket => { + Object.keys(shaft[bearing as TBearing] as Record>).forEach(gasket => { if (!options[gasket]) options[gasket] = ["OPTIONS"] options[gasket].push(bearing) @@ -25,17 +25,15 @@ export const routary_cors: TRoutaryCORS = if (typeof allow_origin === "string") allow_origin = [allow_origin] const origin = intake.req.headers.get("origin") - if (!origin || !allow_origin.includes(origin)) return new Response("", { status: 404 }) + if (!origin || !allow_origin.includes(origin)) return gear(intake) - const headers = { - "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Methods": options[gasket].join(", "), - } as Record + if (!intake.headers) intake.headers = new Headers() - if (max_age) headers["Access-Control-Max-Age"] = String(max_age) - if (allow_headers.length) headers["Access-Control-Allow-Headers"] = allow_headers.join(", ") + intake.headers.set("Access-Control-Allow-Origin", origin) + intake.headers.set("Access-Control-Allow-Methods", options[gasket].join(", ")) - intake.headers = headers + if (max_age) intake.headers.set("Access-Control-Max-Age", String(max_age)) + if (allow_headers.length) intake.headers.set("Access-Control-Allow-Headers", allow_headers.join(", ")) return gear(intake) } @@ -51,13 +49,13 @@ export const routary_cors: TRoutaryCORS = if (!origin || !allow_origin.includes(origin)) return new Response("", { status: 404 }) - const headers = { - "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Methods": options[gasket].join(", "), - } as Record + const headers = new Headers() - if (max_age) headers["Access-Control-Max-Age"] = String(max_age) - if (allow_headers.length) headers["Access-Control-Allow-Headers"] = allow_headers.join(", ") + headers.set("Access-Control-Allow-Origin", origin) + headers.set("Access-Control-Allow-Methods", options[gasket].join(", ")) + + if (max_age) headers.set("Access-Control-Max-Age", String(max_age)) + if (allow_headers.length) headers.set("Access-Control-Allow-Headers", allow_headers.join(", ")) return new Response("", { status: success_status, headers }) } diff --git a/lib/routary-cors/src/routary-cors.types.ts b/lib/routary-cors/src/routary-cors.types.ts index 00eb7db4a..10194bd55 100644 --- a/lib/routary-cors/src/routary-cors.types.ts +++ b/lib/routary-cors/src/routary-cors.types.ts @@ -12,6 +12,6 @@ export type TRoutaryCORSParams = { allow_headers?: string[] } -export type TRoutaryCORS = <$TChamber extends Record & { headers: Record }>( +export type TRoutaryCORS = <$TChamber extends Record & { headers: Headers }>( params: TRoutaryCORSParams, ) => Parameters["use"]>[0] diff --git a/lib/routary/src/routary.types.ts b/lib/routary/src/routary.types.ts index 1cd1b8658..36c2a1a9c 100644 --- a/lib/routary/src/routary.types.ts +++ b/lib/routary/src/routary.types.ts @@ -33,5 +33,5 @@ export type TRoutary<$TChamber> = { head: (gasket: TGasket, gear: TGear<$TChamber>) => TRoutary<$TChamber> options: (gasket: TGasket, gear: TGear<$TChamber>) => TRoutary<$TChamber> each: (gasket: TGasket, bearings: TBearing[], gear: TGear<$TChamber>) => TRoutary<$TChamber> - start: (chown_gear: TGear<$TChamber>) => (req: Request, server: Server) => Response | Promise + start: (crown_gear: TGear<$TChamber>) => (req: Request, server: Server) => Response | Promise } diff --git a/lib/tau/src/impl.ts b/lib/tau/src/impl.ts index e242addff..d9a6608da 100644 --- a/lib/tau/src/impl.ts +++ b/lib/tau/src/impl.ts @@ -147,9 +147,9 @@ export const first_matched = (xs: T[]) => xs.find(i => f(i)) -type TProp = <$Key extends PropertyKey>( - prop: $Key, -) => <$Record extends { [_Property in $Key]: unknown }>(obj: $Record) => $Record[$Key] +type TProp = <$TRecord extends Record | any[], $TKey extends keyof $TRecord>( + prop: $TKey, +) => (obj: $TRecord) => $TRecord[$TKey] export const prop: TProp = key => obj => obj[key] export const thunk = diff --git a/package.json b/package.json index fc9105b38..aa7debde6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "name": "@ordo-pink/ordo", "devDependencies": { - "@types/bun": "^1.1.11", + "@types/bun": "^1.2.2", "@types/eslint": "^8.56.2", "@types/koa-bodyparser": "^4.3.10", "@types/koa-cors": "^0.0.2", diff --git a/root/bruno/DT/Create File Content.bru b/root/bruno/DT/Create File Content.bru new file mode 100644 index 000000000..f56099e45 --- /dev/null +++ b/root/bruno/DT/Create File Content.bru @@ -0,0 +1,28 @@ +meta { + name: Create File Content + type: http + seq: 1 +} + +post { + url: {{DT_HOST}}/:uid/:fsid + body: text + auth: bearer +} + +params:path { + uid: {{CURRENT_ID}} + fsid: {{CURRENT_ID}} +} + +headers { + Origin: {{ORIGIN}} +} + +auth:bearer { + token: {{CURRENT_TOKEN}} +} + +body:text { + Hello world +} diff --git a/root/bruno/DT/Delete File Content.bru b/root/bruno/DT/Delete File Content.bru new file mode 100644 index 000000000..8733d6325 --- /dev/null +++ b/root/bruno/DT/Delete File Content.bru @@ -0,0 +1,24 @@ +meta { + name: Delete File Content + type: http + seq: 5 +} + +delete { + url: {{DT_HOST}}/:uid/:fsid + body: none + auth: bearer +} + +params:path { + uid: {{CURRENT_ID}} + fsid: {{CURRENT_ID}} +} + +headers { + Origin: {{ORIGIN}} +} + +auth:bearer { + token: {{CURRENT_TOKEN}} +} diff --git a/root/bruno/DT/Get File Content.bru b/root/bruno/DT/Get File Content.bru new file mode 100644 index 000000000..5bc6604be --- /dev/null +++ b/root/bruno/DT/Get File Content.bru @@ -0,0 +1,24 @@ +meta { + name: Get File Content + type: http + seq: 3 +} + +get { + url: {{DT_HOST}}/:uid/:fsid + body: none + auth: bearer +} + +params:path { + uid: {{CURRENT_ID}} + fsid: {{CURRENT_ID}} +} + +headers { + Origin: {{ORIGIN}} +} + +auth:bearer { + token: {{CURRENT_TOKEN}} +} diff --git a/root/bruno/DT/Get File Modification Time.bru b/root/bruno/DT/Get File Modification Time.bru new file mode 100644 index 000000000..df2b0e160 --- /dev/null +++ b/root/bruno/DT/Get File Modification Time.bru @@ -0,0 +1,24 @@ +meta { + name: Get File Modification Time + type: http + seq: 2 +} + +head { + url: {{DT_HOST}}/:uid/:fsid + body: none + auth: bearer +} + +params:path { + uid: {{CURRENT_ID}} + fsid: "b68678af-6776-47c0-a0b8-4b6535664b8c" +} + +headers { + Origin: {{ORIGIN}} +} + +auth:bearer { + token: {{CURRENT_TOKEN}} +} diff --git a/root/bruno/DT/Update File Content.bru b/root/bruno/DT/Update File Content.bru new file mode 100644 index 000000000..332457c2d --- /dev/null +++ b/root/bruno/DT/Update File Content.bru @@ -0,0 +1,28 @@ +meta { + name: Update File Content + type: http + seq: 4 +} + +put { + url: {{DT_HOST}}/:uid/:fsid + body: text + auth: bearer +} + +params:path { + uid: {{CURRENT_ID}} + fsid: {{CURRENT_ID}} +} + +headers { + Origin: {{ORIGIN}} +} + +auth:bearer { + token: {{CURRENT_TOKEN}} +} + +body:text { + Hello world! +} diff --git a/root/bruno/DT/[pub] Healthcheck.bru b/root/bruno/DT/[pub] Healthcheck.bru new file mode 100644 index 000000000..5d26aa4fe --- /dev/null +++ b/root/bruno/DT/[pub] Healthcheck.bru @@ -0,0 +1,15 @@ +meta { + name: [pub] Healthcheck + type: http + seq: 6 +} + +get { + url: {{DT_HOST}}/healthcheck + body: none + auth: none +} + +headers { + Origin: {{ORIGIN}} +} diff --git a/root/bruno/ID/codes/[pub] Validate Code.bru b/root/bruno/ID/codes/[pub] Validate Code.bru index e7f6adab2..3dad4af8f 100644 --- a/root/bruno/ID/codes/[pub] Validate Code.bru +++ b/root/bruno/ID/codes/[pub] Validate Code.bru @@ -21,7 +21,7 @@ headers { body:json { { "email": "{{USER_EMAIL}}", - "code": "143948" + "code": "{{CURRENT_CODE}}" } } diff --git a/root/bruno/collection.bru b/root/bruno/collection.bru index 0d53de500..5f2795e4d 100644 --- a/root/bruno/collection.bru +++ b/root/bruno/collection.bru @@ -4,4 +4,7 @@ vars:pre-request { ORIGIN: http://localhost:3004 USER_EMAIL: test@test.com USER_HANDLE: @test + DT_PORT: 3002 + DT_HOST: http://localhost:{{DT_PORT}} + CURRENT_CODE: 199515 } diff --git a/srv/data/Dockerfile b/srv/data/Dockerfile deleted file mode 100644 index c5a17cdf6..000000000 --- a/srv/data/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM ubuntu:latest - -ARG ORDO_DT_DOCKER_PORT -ARG ORDO_DT_DOCKER_ENV -ARG ORDO_DT_DOCKER_REGION - -MAINTAINER 谢尔盖||↓ and the Ordo.pink contributors -LABEL pink.ordo.service="DT" -LABEL pink.ordo.description="Docker image for Ordo DT service" -LABEL pink.ordo.env=${ORDO_DT_DOCKER_ENV} -LABEL pink.ordo.region=${ORDO_DT_DOCKER_REGION} - -EXPOSE ${ORDO_DT_DOCKER_PORT:-3002} - -COPY ./var/out/dt /dt - -ENTRYPOINT ["/dt"] diff --git a/srv/data/bin/compile.ts b/srv/data/bin/compile.ts deleted file mode 100644 index 9885ec979..000000000 --- a/srv/data/bin/compile.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { create_dir_if_not_exists0, mv0 } from "@ordo-pink/fs" -import { die, run_bun_command } from "@ordo-pink/binutil" -import { getc } from "@ordo-pink/getc" -import { keys_of } from "@ordo-pink/tau" - -const env = getc() - -const defineEnv = (env: Record) => - keys_of(env).reduce((acc, key) => acc.concat(`--define Bun.env.${key}='${env[key]}' `), "") -const createOutDirectoryIfNotExists0 = () => create_dir_if_not_exists0("var/out") -const moveCompiledFileToOutDirectory0 = () => mv0("dt", "var/out/dt") - -const command = "build srv/data/index.ts --outfile=dt --target=bun --minify --compile " -const envDefinitions = defineEnv(env) - -void run_bun_command(command.concat(envDefinitions)) - .chain(createOutDirectoryIfNotExists0) - .chain(moveCompiledFileToOutDirectory0) - .orElse(die()) diff --git a/srv/data/bin/init.ts b/srv/data/bin/init.ts deleted file mode 100644 index 2f10af4a7..000000000 --- a/srv/data/bin/init.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { Oath, invokers0 } from "@ordo-pink/oath" -import { create_dir_if_not_exists0 } from "@ordo-pink/fs" -import { die } from "@ordo-pink/binutil" -import { getc } from "@ordo-pink/getc" - -const { ORDO_DT_DATA_PATH, ORDO_DT_CONTENT_PATH } = getc(["ORDO_DT_DATA_PATH", "ORDO_DT_CONTENT_PATH"]) - -void Oath.Merge([create_dir_if_not_exists0(ORDO_DT_DATA_PATH), create_dir_if_not_exists0(ORDO_DT_CONTENT_PATH)]).invoke( - invokers0.or_else(die()), -) diff --git a/srv/data/bin/publish.ts b/srv/data/bin/publish.ts deleted file mode 100644 index 91563ca26..000000000 --- a/srv/data/bin/publish.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { die, run_command } from "@ordo-pink/binutil" -import { getc } from "@ordo-pink/getc" - -const { - ORDO_DT_ENV, - ORDO_DT_PORT, - ORDO_DT_REGION, - ORDO_DT_DOCKER_REGISTRY, - ORDO_DT_DOCKER_REGISTRY_SCOPE, - ORDO_DT_DOCKER_REGISTRY_USERNAME, - ORDO_DT_DOCKER_REGISTRY_PASSWORD, - ORDO_DT_VERSION, -} = getc([ - "ORDO_DT_PORT", - "ORDO_DT_ENV", - "ORDO_DT_REGION", - "ORDO_DT_DOCKER_REGISTRY", - "ORDO_DT_DOCKER_REGISTRY_SCOPE", - "ORDO_DT_DOCKER_REGISTRY_USERNAME", - "ORDO_DT_DOCKER_REGISTRY_PASSWORD", - "ORDO_DT_VERSION", -]) - -const build = `docker build --build-arg ORDO_DT_DOCKER_PORT=${ORDO_DT_PORT} --build-arg ORDO_DT_DOCKER_ENV=${ORDO_DT_ENV} --build-arg ORDO_DT_DOCKER_REGION=${ORDO_DT_REGION} -t ${ORDO_DT_DOCKER_REGISTRY}/${ORDO_DT_DOCKER_REGISTRY_SCOPE}/dt:${ORDO_DT_VERSION} -f ./srv/data/Dockerfile .` -const login = `docker login --username ${ORDO_DT_DOCKER_REGISTRY_USERNAME} --password ${ORDO_DT_DOCKER_REGISTRY_PASSWORD} ${ORDO_DT_DOCKER_REGISTRY}` -const publish = `docker push ${ORDO_DT_DOCKER_REGISTRY}/${ORDO_DT_DOCKER_REGISTRY_SCOPE}/dt:${ORDO_DT_VERSION}` - -void run_command(build) - .chain(() => run_command(login)) - .chain(() => run_command(publish)) - .orElse(die()) diff --git a/srv/data/index.ts b/srv/data/index.ts deleted file mode 100644 index c845c55a0..000000000 --- a/srv/data/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors - * SPDX-License-Identifier: AGPL-3.0-only - * - * Ordo.pink is an all-in-one team workspace. - * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import chalk from "chalk" - -import { ConsoleLogger } from "@ordo-pink/logger" -import { ContentPersistenceStrategyFS } from "@ordo-pink/backend-persistence-strategy-content-fs" -import { ContentPersistenceStrategyS3 } from "@ordo-pink/backend-persistence-strategy-content-s3" -import { DataCommands } from "@ordo-pink/managers" -import { DataPersistenceStrategyFS } from "@ordo-pink/backend-persistence-strategy-data-fs" -import { DataPersistenceStrategyS3 } from "@ordo-pink/backend-persistence-strategy-data-s3" -import { Switch } from "@ordo-pink/switch" -import { createDataServer } from "@ordo-pink/backend-server-data" - -const port = Bun.env.ORDO_DT_PORT! -const idHost = Bun.env.ORDO_ID_HOST! -const origin = [Bun.env.ORDO_WORKSPACE_HOST!, Bun.env.ORDO_WEB_HOST!] -const logger = ConsoleLogger -const contentPersistenceStrategyType = Bun.env.ORDO_DT_CONTENT_PERSISTENCE_STRATEGY! - -const dataPersistenceStrategyType = Bun.env.ORDO_DT_DATA_PERSISTENCE_STRATEGY! - -const dataPersistenceStrategyFSParams = { - root: Bun.env.ORDO_DT_DATA_PATH!, -} - -const dataPersistenceStrategyS3Params = { - accessKeyId: Bun.env.ORDO_DT_DATA_S3_ACCESS_KEY!, - secretAccessKey: Bun.env.ORDO_DT_DATA_S3_SECRET_KEY!, - region: Bun.env.ORDO_DT_DATA_S3_REGION!, - endpoint: Bun.env.ORDO_DT_DATA_S3_ENDPOINT!, - bucketName: Bun.env.ORDO_DT_DATA_S3_BUCKET_NAME!, -} - -const contentPersistenceStrategyFSParams = { - root: Bun.env.ORDO_DT_CONTENT_PATH!, -} - -const contentPersistenceStrategyS3Params = { - accessKeyId: Bun.env.ORDO_DT_CONTENT_S3_ACCESS_KEY!, - secretAccessKey: Bun.env.ORDO_DT_CONTENT_S3_SECRET_KEY!, - region: Bun.env.ORDO_DT_CONTENT_S3_REGION!, - endpoint: Bun.env.ORDO_DT_CONTENT_S3_ENDPOINT!, - bucketName: Bun.env.ORDO_DT_CONTENT_S3_BUCKET_NAME!, -} - -const main = () => { - const dataPersistenceStrategy = Switch.of(dataPersistenceStrategyType) - .case("s3", () => DataPersistenceStrategyS3.of(dataPersistenceStrategyS3Params)) - .default(() => DataPersistenceStrategyFS.of(dataPersistenceStrategyFSParams)) - - const contentPersistenceStrategy = Switch.of(contentPersistenceStrategyType) - .case("s3", () => ContentPersistenceStrategyS3.of(contentPersistenceStrategyS3Params)) - .default(() => ContentPersistenceStrategyFS.of(contentPersistenceStrategyFSParams)) - - const dataService = DataCommands.of({ dataPersistenceStrategy, contentPersistenceStrategy }) - - createDataServer({ dataService, idHost, origin, logger }).listen({ port: Number(port) }, () => - ConsoleLogger.info(`DT running on http://localhost:${chalk.blue(port)}`), - ) -} - -main() diff --git a/srv/data/license b/srv/data/license deleted file mode 100644 index 3a64ee5fe..000000000 --- a/srv/data/license +++ /dev/null @@ -1,504 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy -and distribute verbatim copies of this license document, but changing it is not allowed. - - Preamble - -The GNU Affero General Public License is a free, copyleft license for software and other kinds of -works, specifically designed to ensure cooperation with the community in the case of network server -software. - -The licenses for most software and other practical works are designed to take away your freedom to -share and change the works. By contrast, our General Public Licenses are intended to guarantee your -freedom to share and change all versions of a program--to make sure it remains free software for all -its users. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses -are designed to make sure that you have the freedom to distribute copies of free software (and -charge for them if you wish), that you receive source code or can get it if you want it, that you -can change the software or use pieces of it in new free programs, and that you know you can do these -things. - -Developers that use our General Public Licenses protect your rights with two steps: (1) assert -copyright on the software, and (2) offer you this License which gives you legal permission to copy, -distribute and/or modify the software. - -A secondary benefit of defending all users' freedom is that improvements made in alternate versions -of the program, if they receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and encouraged by the resulting -cooperation. However, in the case of software used on network servers, this result may fail to come -about. The GNU General Public License permits making a modified version and letting the public -access it on a server without ever releasing its source code to the public. - -The GNU Affero General Public License is designed specifically to ensure that, in such cases, the -modified source code becomes available to the community. It requires the operator of a network -server to provide the source code of the modified version running there to the users of that server. -Therefore, public use of a modified version, on a publicly accessible server, gives the public -access to the source code of the modified version. - -An older license, called the Affero General Public License and published by Affero, was designed to -accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero -has released a new version of the Affero GPL which permits relicensing under this license. - -The precise terms and conditions for copying, distribution and modification follow. - - TERMS AND CONDITIONS - -0. Definitions. - -"This License" refers to version 3 of the GNU Affero General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor -masks. - -"The Program" refers to any copyrightable work licensed under this License. Each licensee is -addressed as "you". "Licensees" and "recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring -copyright permission, other than the making of an exact copy. The resulting work is called a -"modified version" of the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the Program. - -To "propagate" a work means to do anything with it that, without permission, would make you directly -or secondarily liable for infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, distribution (with or without -modification), making available to the public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other parties to make or receive -copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a -convenient and prominently visible feature that (1) displays an appropriate copyright notice, and -(2) tells the user that there is no warranty for the work (except to the extent that warranties are -provided), that licensees may convey the work under this License, and how to view a copy of this -License. If the interface presents a list of user commands or options, such as a menu, a prominent -item in the list meets this criterion. - -1. Source Code. - -The "source code" for a work means the preferred form of the work for making modifications to it. -"Object code" means any non-source form of a work. - -A "Standard Interface" means an interface that either is an official standard defined by a -recognized standards body, or, in the case of interfaces specified for a particular programming -language, one that is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other than the work as a whole, that -(a) is included in the normal form of packaging a Major Component, but which is not part of that -Major Component, and (b) serves only to enable use of the work with that Major Component, or to -implement a Standard Interface for which an implementation is available to the public in source code -form. A "Major Component", in this context, means a major essential component (kernel, window -system, and so on) of the specific operating system (if any) on which the executable work runs, or a -compiler used to produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source code needed to -generate, install, and (for an executable work) run the object code and to modify the work, -including scripts to control those activities. However, it does not include the work's System -Libraries, or general-purpose tools or generally available free programs which are used unmodified -in performing those activities but which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for the work, and the source code -for shared libraries and dynamically linked subprograms that the work is specifically designed to -require, such as by intimate data communication or control flow between those subprograms and other -parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from -other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. - -All rights granted under this License are granted for the term of copyright on the Program, and are -irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a covered work is covered by this -License only if the output, given its content, constitutes a covered work. This License acknowledges -your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as -your license otherwise remains in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you with facilities for running -those works, provided that you comply with the terms of this License in conveying all material for -which you do not control copyright. Those thus making or running the covered works for you must do -so exclusively on your behalf, under your direction and control, on terms that prohibit them from -making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. -Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological measure under any applicable law -fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological -measures to the extent such circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit operation or modification of -the work as a means of enforcing, against the work's users, your or third parties' legal rights to -forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you receive it, in any medium, -provided that you conspicuously and appropriately publish on each copy an appropriate copyright -notice; keep intact all notices stating that this License and any non-permissive terms added in -accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and -give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or -warranty protection for a fee. - -5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to produce it from the Program, in -the form of source code under the terms of section 4, provided that you also meet all of these -conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their -nature extensions of the covered work, and which are not combined with it such as to form a larger -program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the -compilation and its resulting copyright are not used to limit the access or legal rights of the -compilation's users beyond what the individual works permit. Inclusion of a covered work in an -aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that -you also convey the machine-readable Corresponding Source under the terms of this License, in one of -these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source -as a System Library, need not be included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible personal property -which is normally used for personal, family, or household purposes, or (2) anything designed or sold -for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful -cases shall be resolved in favor of coverage. For a particular product received by a particular -user, "normally used" refers to a typical or common use of that class of product, regardless of the -status of the particular user or of the way in which the particular user actually uses, or expects -or is expected to use, the product. A product is a consumer product regardless of whether the -product has substantial commercial, industrial or non-consumer uses, unless such uses represent the -only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, authorization keys, or -other information required to install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The information must suffice to ensure -that the continued functioning of the modified object code is in no case prevented or interfered -with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User -Product, and the conveying occurs as part of a transaction in which the right of possession and use -of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of -how the transaction is characterized), the Corresponding Source conveyed under this section must be -accompanied by the Installation Information. But this requirement does not apply if neither you nor -any third party retains the ability to install modified object code on the User Product (for -example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to -provide support service, warranty, or updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or installed. Access to a network -may be denied when the modification itself materially and adversely affects the operation of the -network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section -must be in a format that is publicly documented (and with an implementation available to the public -in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this License by making exceptions -from one or more of its conditions. Additional permissions that are applicable to the entire Program -shall be treated as though they were included in this License, to the extent that they are valid -under applicable law. If additional permissions apply only to part of the Program, that part may be -used separately under those permissions, but the entire Program remains governed by this License -without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions -from that copy, or from any part of it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place additional permissions on -material, added by you to a covered work, for which you have or can give appropriate copyright -permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may -(if authorized by the copyright holders of that material) supplement the terms of this License with -terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - -All other non-permissive additional terms are considered "further restrictions" within the meaning -of section 10. If the Program as you received it, or any part of it, contains a notice stating that -it is governed by this License along with a term that is a further restriction, you may remove that -term. If a license document contains a further restriction but permits relicensing or conveying -under this License, you may add to a covered work material governed by the terms of that license -document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant -source files, a statement of the additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written -license, or stated as exceptions; the above requirements apply either way. - -8. Termination. - -You may not propagate or modify a covered work except as expressly provided under this License. Any -attempt otherwise to propagate or modify it is void, and will automatically terminate your rights -under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright -holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder fails to notify you of the -violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright -holder notifies you of the violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that copyright holder, and you cure -the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have -received copies or rights from you under this License. If your rights have been terminated and not -permanently reinstated, you do not qualify to receive new licenses for the same material under -section 10. - -9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run a copy of the Program. -Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer -transmission to receive a copy likewise does not require acceptance. However, nothing other than -this License grants you permission to propagate or modify any covered work. These actions infringe -copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, -you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically receives a license from the -original licensors, to run, modify and propagate that work, subject to this License. You are not -responsible for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an organization, or substantially -all assets of one, or subdividing an organization, or merging organizations. If propagation of a -covered work results from an entity transaction, each party to that transaction who receives a copy -of the work also receives whatever licenses to the work the party's predecessor in interest had or -could give under the previous paragraph, plus a right to possession of the Corresponding Source of -the work from the predecessor in interest, if the predecessor has it or can get it with reasonable -efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under -this License. For example, you may not impose a license fee, royalty, or other charge for exercise -of rights granted under this License, and you may not initiate litigation (including a cross-claim -or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, -offering for sale, or importing the Program or any portion of it. - -11. Patents. - -A "contributor" is a copyright holder who authorizes use under this License of the Program or a work -on which the Program is based. The work thus licensed is called the contributor's "contributor -version". - -A contributor's "essential patent claims" are all patent claims owned or controlled by the -contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, -permitted by this License, of making, using, or selling its contributor version, but do not include -claims that would be infringed only as a consequence of further modification of the contributor -version. For purposes of this definition, "control" includes the right to grant patent sublicenses -in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the -contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, -modify and propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement or commitment, -however denominated, not to enforce a patent (such as an express permission to practice a patent or -covenant not to sue for patent infringement). To "grant" such a patent license to a party means to -make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of -the work is not available for anyone to copy, free of charge and under the terms of this License, -through a publicly available network server or other readily accessible means, then you must either -(1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the -benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with -the requirements of this License, to extend the patent license to downstream recipients. "Knowingly -relying" means you have actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work in a country, would infringe -one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate -by procuring conveyance of, a covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of -the covered work, then the patent license you grant is automatically extended to all recipients of -the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of its coverage, -prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that -are specifically granted under this License. You may not convey a covered work if you are a party to -an arrangement with a third party that is in the business of distributing software, under which you -make payment to the third party based on the extent of your activity of conveying the work, and -under which the third party grants, to any of the parties who would receive the covered work from -you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in connection with specific -products or compilations that contain the covered work, unless you entered into that arrangement, or -that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other -defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict -the conditions of this License, they do not excuse you from the conditions of this License. If you -cannot convey a covered work so as to satisfy simultaneously your obligations under this License and -any other pertinent obligations, then as a consequence you may not convey it at all. For example, if -you agree to terms that obligate you to collect a royalty for further conveying from those to whom -you convey the Program, the only way you could satisfy both those terms and this License would be to -refrain entirely from conveying the Program. - -13. Remote Network Interaction; Use with the GNU General Public License. - -Notwithstanding any other provision of this License, if you modify the Program, your modified -version must prominently offer all users interacting with it remotely through a computer network (if -your version supports such interaction) an opportunity to receive the Corresponding Source of your -version by providing access to the Corresponding Source from a network server at no charge, through -some standard or customary means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 of the GNU General Public -License that is incorporated pursuant to the following paragraph. - -Notwithstanding any other provision of this License, you have permission to link or combine any -covered work with a work licensed under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this License will continue to apply to -the part which is the covered work, but the work with which it is combined will remain governed by -version 3 of the GNU General Public License. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of the GNU Affero General -Public License from time to time. Such new versions will be similar in spirit to the present -version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain -numbered version of the GNU Affero General Public License "or any later version" applies to it, you -have the option of following the terms and conditions either of that numbered version or of any -later version published by the Free Software Foundation. If the Program does not specify a version -number of the GNU Affero General Public License, you may choose any version ever published by the -Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU Affero General -Public License can be used, that proxy's public statement of acceptance of a version permanently -authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional -obligations are imposed on any author or copyright holder as a result of your choosing to follow a -later version. - -15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" -WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO -THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU -ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR -ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR -DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE -OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED -INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH -ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - -17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal -effect according to their terms, reviewing courts shall apply local law that most closely -approximates an absolute waiver of all civil liability in connection with the Program, unless a -warranty or assumption of liability accompanies a copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS diff --git a/srv/data/readme.md b/srv/data/readme.md deleted file mode 100644 index 599c41b4d..000000000 --- a/srv/data/readme.md +++ /dev/null @@ -1 +0,0 @@ -# Data diff --git a/srv/data/bin/run.ts b/srv/dt/bin/run.ts similarity index 94% rename from srv/data/bin/run.ts rename to srv/dt/bin/run.ts index bc6e9efe3..9d8972e33 100644 --- a/srv/data/bin/run.ts +++ b/srv/dt/bin/run.ts @@ -22,7 +22,7 @@ import { die, run_async_command } from "@ordo-pink/binutil" import { invokers0 } from "@ordo-pink/oath" -void run_async_command("opt/bun run --watch srv/data/index.ts", { +void run_async_command("opt/bun run --watch srv/dt/index.ts", { stdout: "pipe", stderr: "pipe", env: { ...process.env, FORCE_COLOR: "1" }, diff --git a/srv/dt/index.ts b/srv/dt/index.ts new file mode 100644 index 000000000..81c4432be --- /dev/null +++ b/srv/dt/index.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { ConsoleLogger, type TLogger } from "@ordo-pink/logger" +import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { type TDTChamber, create_backend_dt } from "@ordo-pink/backend-dt" +import { PersistenceStrategyDataFS } from "@ordo-pink/backend-persistence-strategy-data-fs" +import { is_port } from "@ordo-pink/tau" + +const env_rrr = (env_var: string) => (value?: any) => + value != null ? `Invalid value for ${env_var}: "${value}"` : `Missing value for ${env_var}` + +const get_env = () => + Oath.Merge({ + port: Oath.FromNullable(Bun.env.ORDO_DT_PORT) + .and(n => Oath.If(is_port(n), { T: () => n })) + .pipe(ops0.rejected_map(env_rrr("ORDO_DT_PORT"))), + + allow_origin: Oath.FromNullable(Bun.env.ORDO_DT_ALLOW_ORIGIN) + .and(s => s.split(", ")) + .pipe(ops0.rejected_map(env_rrr("ORDO_DT_ALLOW_ORIGIN"))), + + data_path: Oath.FromNullable(Bun.env.ORDO_DT_DATA_PATH, env_rrr("ORDO_DT_DATA_PATH")), + + id_host: Oath.FromNullable(Bun.env.ORDO_ID_HOST, env_rrr("ORDO_ID_HOST")), + + dt_host: Oath.FromNullable(Bun.env.ORDO_DT_HOST, env_rrr("ORDO_DT_HOST")), + }) + +const main = () => + get_env() + .and(({ allow_origin, port, data_path, id_host, dt_host }) => + Oath.Merge({ + allow_origin, + logger, + data_persistence_strategy: PersistenceStrategyDataFS.Of(data_path), + id_host, + dt_host, + } satisfies TDTChamber) + .and(create_backend_dt) + .and(fetch => Bun.serve({ fetch, port })), + ) + .pipe(ops0.tap(server => logger.info(`server running on http://${server.hostname}:${server.port}`))) + .invoke( + invokers0.or_else(e => { + logger.panic(e) + process.exit(1) + }), + ) + +void main() + +// --- Internal --- + +const logger: TLogger = { + alert: (...message) => ConsoleLogger.alert("[DT]", ...message), + crit: (...message) => ConsoleLogger.crit("[DT]", ...message), + debug: (...message) => ConsoleLogger.debug("[DT]", ...message), + error: (...message) => ConsoleLogger.error("[DT]", ...message), + info: (...message) => ConsoleLogger.info("[DT]", ...message), + notice: (...message) => ConsoleLogger.notice("[DT]", ...message), + panic: (...message) => ConsoleLogger.panic("[DT]", ...message), + warn: (...message) => ConsoleLogger.warn("[DT]", ...message), +} diff --git a/lib/backend-email-strategy-rusender/license b/srv/dt/license similarity index 100% rename from lib/backend-email-strategy-rusender/license rename to srv/dt/license diff --git a/srv/dt/readme.md b/srv/dt/readme.md new file mode 100644 index 000000000..990436c5c --- /dev/null +++ b/srv/dt/readme.md @@ -0,0 +1,3 @@ +# DT + +This is a data service instance used for local development. diff --git a/srv/id/index.ts b/srv/id/index.ts index 4d9f49c9b..d369c2dfb 100644 --- a/srv/id/index.ts +++ b/srv/id/index.ts @@ -88,6 +88,8 @@ const get_env = () => .pipe(ops0.rejected_map(env_rrr("ORDO_ID_PERSISTED_TOKEN_LIFETIME"))), user_db_path: Oath.FromNullable(Bun.env.ORDO_ID_USER_DB_PATH, env_rrr("ORDO_ID_USER_DB_PATH")), + web_host: Oath.FromNullable(Bun.env.ORDO_WEB_HOST, env_rrr("ORDO_WEB_HOST")), + dt_host: Oath.FromNullable(Bun.env.ORDO_DT_HOST, env_rrr("ORDO_DT_HOST")), token_db_path: Oath.FromNullable(Bun.env.ORDO_ID_TOKEN_DB_PATH, env_rrr("ORDO_ID_TOKEN_DB_PATH")), }) @@ -108,6 +110,7 @@ const main = () => token_db_path, token_lifetime, user_db_path, + web_host, }) => Oath.Merge({ allow_origin, @@ -118,8 +121,7 @@ const main = () => token_persistence_strategy: PersistenceStrategyTokenFS.Of(token_db_path), // TODO user_persistence_strategy: PersistenceStategyUserFS.Of(user_db_path), wjwt: WJWT({ aud, alg, private_key, public_key, iss, token_lifetime }), - status: 200, - headers: {}, + web_host, } satisfies TIDChamber) .and(create_backend_id) .and(fetch => Bun.serve({ fetch, port })), diff --git a/lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.types.ts b/srv/pb/bin/run.ts similarity index 76% rename from lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.types.ts rename to srv/pb/bin/run.ts index f66df0310..f4db07ed7 100644 --- a/lib/backend-persistence-strategy-content-fs/src/backend-persistence-strategy-content-fs.types.ts +++ b/srv/pb/bin/run.ts @@ -19,4 +19,11 @@ * along with this program. If not, see . */ -export type TPersistenceStrategyContentFSParams = { root: string } +import { die, run_async_command } from "@ordo-pink/binutil" +import { invokers0 } from "@ordo-pink/oath" + +void run_async_command("opt/bun run --watch srv/pb/index.ts", { + stdout: "pipe", + stderr: "pipe", + env: { ...process.env, FORCE_COLOR: "1" }, +}).invoke(invokers0.or_else(die())) diff --git a/srv/pb/index.ts b/srv/pb/index.ts new file mode 100644 index 000000000..d976af68b --- /dev/null +++ b/srv/pb/index.ts @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright 2024, 谢尔盖 ||↓ and the Ordo.pink contributors + * SPDX-License-Identifier: AGPL-3.0-only + * + * Ordo.pink is an all-in-one team workspace. + * Copyright (C) 2024 谢尔盖 ||↓ and the Ordo.pink contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { ConsoleLogger, type TLogger } from "@ordo-pink/logger" +import { Oath, invokers0, ops0 } from "@ordo-pink/oath" +import { PersistenceStrategyDataFS } from "@ordo-pink/backend-persistence-strategy-data-fs" +import { create_backend_pb } from "@ordo-pink/backend-pb" +import { is_port } from "@ordo-pink/tau" + +const env_rrr = (env_var: string) => (value?: any) => + value != null ? `Invalid value for ${env_var}: "${value}"` : `Missing value for ${env_var}` + +const get_env = () => + Oath.Merge({ + port: Oath.FromNullable(Bun.env.ORDO_PB_PORT) + .and(n => Oath.If(is_port(n), { T: () => n })) + .pipe(ops0.rejected_map(env_rrr("ORDO_PB_PORT"))), + + data_path: Oath.FromNullable(Bun.env.ORDO_PB_DATA_PATH, env_rrr("ORDO_PB_DATA_PATH")), + + allow_origin: Oath.FromNullable(Bun.env.ORDO_DT_ALLOW_ORIGIN) + .and(s => s.split(", ")) + .pipe(ops0.rejected_map(env_rrr("ORDO_DT_ALLOW_ORIGIN"))), + }) + +const main = () => + get_env() + .and(({ port, data_path, allow_origin }) => + Oath.Merge({ logger, data_persistence_strategy: PersistenceStrategyDataFS.Of(data_path), allow_origin }) + .and(create_backend_pb) + .and(fetch => Bun.serve({ fetch, port })), + ) + .pipe(ops0.tap(server => logger.info(`server running on http://${server.hostname}:${server.port}`))) + .invoke( + invokers0.or_else(e => { + logger.panic(e) + process.exit(1) + }), + ) + +void main() + +// --- Internal --- + +const logger: TLogger = { + alert: (...message) => ConsoleLogger.alert("[PB]", ...message), + crit: (...message) => ConsoleLogger.crit("[PB]", ...message), + debug: (...message) => ConsoleLogger.debug("[PB]", ...message), + error: (...message) => ConsoleLogger.error("[PB]", ...message), + info: (...message) => ConsoleLogger.info("[PB]", ...message), + notice: (...message) => ConsoleLogger.notice("[PB]", ...message), + panic: (...message) => ConsoleLogger.panic("[PB]", ...message), + warn: (...message) => ConsoleLogger.warn("[PB]", ...message), +} diff --git a/lib/backend-persistence-strategy-content-fs/license b/srv/pb/license similarity index 100% rename from lib/backend-persistence-strategy-content-fs/license rename to srv/pb/license diff --git a/srv/pb/readme.md b/srv/pb/readme.md new file mode 100644 index 000000000..990436c5c --- /dev/null +++ b/srv/pb/readme.md @@ -0,0 +1,3 @@ +# DT + +This is a data service instance used for local development. diff --git a/srv/web/bin/run.ts b/srv/web/bin/run.ts index e9af471aa..2eb5e6f0d 100644 --- a/srv/web/bin/run.ts +++ b/srv/web/bin/run.ts @@ -23,13 +23,7 @@ import { die, run_command } from "@ordo-pink/binutil" import { getc } from "@ordo-pink/getc" import { invokers0 } from "@ordo-pink/oath" -const { ORDO_STATIC_HOST, ORDO_ID_HOST, ORDO_WEB_HOST, ORDO_DT_HOST, ORDO_WORKSPACE_HOST } = getc([ - "ORDO_STATIC_HOST", - "ORDO_ID_HOST", - "ORDO_WEB_HOST", - "ORDO_DT_HOST", - "ORDO_WORKSPACE_HOST", -]) +const { ORDO_ID_HOST, ORDO_DT_HOST, ORDO_PB_HOST } = getc(["ORDO_ID_HOST", "ORDO_DT_HOST", "ORDO_PB_HOST"]) void run_command("npm run dev", { cwd: "./srv/web", @@ -37,11 +31,9 @@ void run_command("npm run dev", { stdout: "inherit", env: { ...process.env, - VITE_ORDO_STATIC_HOST: ORDO_STATIC_HOST, VITE_ORDO_ID_HOST: ORDO_ID_HOST, - VITE_ORDO_WEBSITE_HOST: ORDO_WEB_HOST, + VITE_ORDO_PB_HOST: ORDO_PB_HOST, VITE_ORDO_DT_HOST: ORDO_DT_HOST, - VITE_ORDO_WORKSPACE_HOST: ORDO_WORKSPACE_HOST, FORCE_COLOR: "1", }, }).invoke(invokers0.or_else(die())) diff --git a/srv/web/src/index.ts b/srv/web/src/index.ts index 431308a03..1c53d43da 100644 --- a/srv/web/src/index.ts +++ b/srv/web/src/index.ts @@ -24,4 +24,8 @@ import { Maoka } from "@ordo-pink/maoka" const app = document.getElementById("app")! -void Maoka.dom(app, App) +const id_host = import.meta.env.VITE_ORDO_ID_HOST +const dt_host = import.meta.env.VITE_ORDO_DT_HOST +const pb_host = import.meta.env.VITE_ORDO_PB_HOST + +void Maoka.dom(app, App({ id_host, dt_host, pb_host })) diff --git a/srv/web/src/vite-env.d.ts b/srv/web/src/vite-env.d.ts index e20eb5751..e921669d8 100644 --- a/srv/web/src/vite-env.d.ts +++ b/srv/web/src/vite-env.d.ts @@ -35,5 +35,5 @@ interface ImportMetaEnv { /** * Function Store server host. */ - readonly VITE_ORDO_FS_HOST: string + readonly VITE_ORDO_PB_HOST: string } diff --git a/srv/web/vite.config.ts b/srv/web/vite.config.ts index b666f2aea..53ed42452 100644 --- a/srv/web/vite.config.ts +++ b/srv/web/vite.config.ts @@ -28,7 +28,9 @@ import tsconfigPaths from "vite-tsconfig-paths" // https://vitejs.dev/config/ export default defineConfig({ plugins: [ + // @ts-ignore tsconfigPaths(), + // @ts-ignore ViteImageOptimizer({ png: { quality: 80 }, jpeg: { quality: 75 },