mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react/settings): port etapi tokens
This commit is contained in:
parent
68086ec3f1
commit
c9dcbef014
@ -40,6 +40,7 @@ import ImageSettings from "./options/images.jsx";
|
|||||||
import AdvancedSettings from "./options/advanced.jsx";
|
import AdvancedSettings from "./options/advanced.jsx";
|
||||||
import InternationalizationOptions from "./options/i18n.jsx";
|
import InternationalizationOptions from "./options/i18n.jsx";
|
||||||
import SyncOptions from "./options/sync.jsx";
|
import SyncOptions from "./options/sync.jsx";
|
||||||
|
import EtapiSettings from "./options/etapi.js";
|
||||||
|
|
||||||
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -96,9 +97,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
|
|||||||
ProtectedSessionTimeoutOptions
|
ProtectedSessionTimeoutOptions
|
||||||
],
|
],
|
||||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||||
_optionsEtapi: [
|
_optionsEtapi: <EtapiSettings />,
|
||||||
EtapiOptions
|
|
||||||
],
|
|
||||||
_optionsBackup: [
|
_optionsBackup: [
|
||||||
BackupOptions
|
BackupOptions
|
||||||
],
|
],
|
||||||
|
@ -1,157 +0,0 @@
|
|||||||
import { formatDateTime } from "../../../utils/formatters.js";
|
|
||||||
import { t } from "../../../services/i18n.js";
|
|
||||||
import dialogService from "../../../services/dialog.js";
|
|
||||||
import OptionsWidget from "./options_widget.js";
|
|
||||||
import server from "../../../services/server.js";
|
|
||||||
import toastService from "../../../services/toast.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="etapi-options-section options-section">
|
|
||||||
<h4>${t("etapi.title")}</h4>
|
|
||||||
|
|
||||||
<p class="form-text">${t("etapi.description")} <br/>
|
|
||||||
${t("etapi.see_more", {
|
|
||||||
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
|
|
||||||
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
|
|
||||||
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
|
|
||||||
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button type="button" class="create-etapi-token btn btn-sm">
|
|
||||||
<span class="bx bx-plus"></span>
|
|
||||||
${t("etapi.create_token")}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h5>${t("etapi.existing_tokens")}</h5>
|
|
||||||
|
|
||||||
<div class="no-tokens-yet">${t("etapi.no_tokens_yet")}</div>
|
|
||||||
|
|
||||||
<div style="overflow: auto; height: 500px;">
|
|
||||||
<table class="tokens-table table table-stripped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>${t("etapi.token_name")}</th>
|
|
||||||
<th>${t("etapi.created")}</th>
|
|
||||||
<th>${t("etapi.actions")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.token-table-button {
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 3px;
|
|
||||||
margin-right: 20px;
|
|
||||||
font-size: large;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-table-button:hover {
|
|
||||||
border: 1px solid var(--button-border-color);
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
|
|
||||||
// TODO: Deduplicate
|
|
||||||
interface PostTokensResponse {
|
|
||||||
authToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Deduplicate
|
|
||||||
interface Token {
|
|
||||||
name: string;
|
|
||||||
utcDateCreated: number;
|
|
||||||
etapiTokenId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EtapiOptions extends OptionsWidget {
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.$widget.find(".create-etapi-token").on("click", async () => {
|
|
||||||
const tokenName = await dialogService.prompt({
|
|
||||||
title: t("etapi.new_token_title"),
|
|
||||||
message: t("etapi.new_token_message"),
|
|
||||||
defaultValue: t("etapi.default_token_name")
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tokenName?.trim()) {
|
|
||||||
toastService.showError(t("etapi.error_empty_name"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { authToken } = await server.post<PostTokensResponse>("etapi-tokens", { tokenName });
|
|
||||||
|
|
||||||
await dialogService.prompt({
|
|
||||||
title: t("etapi.token_created_title"),
|
|
||||||
message: t("etapi.token_created_message"),
|
|
||||||
defaultValue: authToken
|
|
||||||
});
|
|
||||||
|
|
||||||
this.refreshTokens();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.refreshTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshTokens() {
|
|
||||||
const $noTokensYet = this.$widget.find(".no-tokens-yet");
|
|
||||||
const $tokensTable = this.$widget.find(".tokens-table");
|
|
||||||
|
|
||||||
const tokens = await server.get<Token[]>("etapi-tokens");
|
|
||||||
|
|
||||||
$noTokensYet.toggle(tokens.length === 0);
|
|
||||||
$tokensTable.toggle(tokens.length > 0);
|
|
||||||
|
|
||||||
const $tokensTableBody = $tokensTable.find("tbody");
|
|
||||||
$tokensTableBody.empty();
|
|
||||||
|
|
||||||
for (const token of tokens) {
|
|
||||||
$tokensTableBody.append(
|
|
||||||
$("<tr>")
|
|
||||||
.append($("<td>").text(token.name))
|
|
||||||
.append($("<td>").text(formatDateTime(token.utcDateCreated)))
|
|
||||||
.append(
|
|
||||||
$("<td>").append(
|
|
||||||
$(`<span class="bx bx-edit-alt token-table-button icon-action" title="${t("etapi.rename_token")}"></span>`).on("click", () => this.renameToken(token.etapiTokenId, token.name)),
|
|
||||||
$(`<span class="bx bx-trash token-table-button icon-action" title="${t("etapi.delete_token")}"></span>`).on("click", () => this.deleteToken(token.etapiTokenId, token.name))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async renameToken(etapiTokenId: string, oldName: string) {
|
|
||||||
const tokenName = await dialogService.prompt({
|
|
||||||
title: t("etapi.rename_token_title"),
|
|
||||||
message: t("etapi.rename_token_message"),
|
|
||||||
defaultValue: oldName
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tokenName?.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.patch(`etapi-tokens/${etapiTokenId}`, { name: tokenName });
|
|
||||||
|
|
||||||
this.refreshTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteToken(etapiTokenId: string, name: string) {
|
|
||||||
if (!(await dialogService.confirm(t("etapi.delete_token_confirmation", { name })))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.remove(`etapi-tokens/${etapiTokenId}`);
|
|
||||||
|
|
||||||
this.refreshTokens();
|
|
||||||
}
|
|
||||||
}
|
|
139
apps/client/src/widgets/type_widgets/options/etapi.tsx
Normal file
139
apps/client/src/widgets/type_widgets/options/etapi.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
import { t } from "../../../services/i18n";
|
||||||
|
import Button from "../../react/Button";
|
||||||
|
import FormText from "../../react/FormText";
|
||||||
|
import RawHtml from "../../react/RawHtml";
|
||||||
|
import OptionsSection from "./components/OptionsSection";
|
||||||
|
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
|
||||||
|
import server from "../../../services/server";
|
||||||
|
import toast from "../../../services/toast";
|
||||||
|
import dialog from "../../../services/dialog";
|
||||||
|
import { formatDateTime } from "../../../utils/formatters";
|
||||||
|
import ActionButton from "../../react/ActionButton";
|
||||||
|
|
||||||
|
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
|
||||||
|
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
|
||||||
|
|
||||||
|
export default function EtapiSettings() {
|
||||||
|
const [ tokens, setTokens ] = useState<EtapiToken[]>([]);
|
||||||
|
|
||||||
|
function refreshTokens() {
|
||||||
|
server.get<EtapiToken[]>("etapi-tokens").then(setTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(refreshTokens, []);
|
||||||
|
|
||||||
|
const createTokenCallback = useCallback(async () => {
|
||||||
|
const tokenName = await dialog.prompt({
|
||||||
|
title: t("etapi.new_token_title"),
|
||||||
|
message: t("etapi.new_token_message"),
|
||||||
|
defaultValue: t("etapi.default_token_name")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenName?.trim()) {
|
||||||
|
toast.showError(t("etapi.error_empty_name"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { authToken } = await server.post<PostTokensResponse>("etapi-tokens", { tokenName });
|
||||||
|
|
||||||
|
await dialog.prompt({
|
||||||
|
title: t("etapi.token_created_title"),
|
||||||
|
message: t("etapi.token_created_message"),
|
||||||
|
defaultValue: authToken
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshTokens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renameTokenCallback = useCallback<RenameTokenCallback>(async (tokenId: string, oldName: string) => {
|
||||||
|
const tokenName = await dialog.prompt({
|
||||||
|
title: t("etapi.rename_token_title"),
|
||||||
|
message: t("etapi.rename_token_message"),
|
||||||
|
defaultValue: oldName
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenName?.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.patch(`etapi-tokens/${tokenId}`, { name: tokenName });
|
||||||
|
|
||||||
|
refreshTokens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteTokenCallback = useCallback<DeleteTokenCallback>(async (tokenId: string, name: string) => {
|
||||||
|
if (!(await dialog.confirm(t("etapi.delete_token_confirmation", { name })))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.remove(`etapi-tokens/${tokenId}`);
|
||||||
|
refreshTokens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OptionsSection title={t("etapi.title")}>
|
||||||
|
<FormText>
|
||||||
|
{t("etapi.description")}<br />
|
||||||
|
<RawHtml
|
||||||
|
html={t("etapi.see_more", {
|
||||||
|
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
|
||||||
|
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
|
||||||
|
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
|
||||||
|
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
|
||||||
|
})} />
|
||||||
|
</FormText>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small" icon="bx bx-plus"
|
||||||
|
text={t("etapi.create_token")}
|
||||||
|
onClick={createTokenCallback}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h5>{t("etapi.existing_tokens")}</h5>
|
||||||
|
<TokenList tokens={tokens} renameCallback={renameTokenCallback} deleteCallback={deleteTokenCallback} />
|
||||||
|
</OptionsSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TokenList({ tokens, renameCallback, deleteCallback }: { tokens: EtapiToken[], renameCallback: RenameTokenCallback, deleteCallback: DeleteTokenCallback }) {
|
||||||
|
if (!tokens.length) {
|
||||||
|
return <div>{t("etapi.no_tokens_yet")}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ overflow: "auto", height: "500px"}}>
|
||||||
|
<table className="table table-stripped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("etapi.token_name")}</th>
|
||||||
|
<th>{t("etapi.created")}</th>
|
||||||
|
<th>{t("etapi.actions")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tokens.map(({ etapiTokenId, name, utcDateCreated}) => (
|
||||||
|
<tr>
|
||||||
|
<td>{name}</td>
|
||||||
|
<td>{formatDateTime(utcDateCreated)}</td>
|
||||||
|
<td>
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-edit-alt"
|
||||||
|
text={t("etapi.rename_token")}
|
||||||
|
onClick={() => renameCallback(etapiTokenId!, name)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-trash"
|
||||||
|
text={t("etapi.delete_token")}
|
||||||
|
onClick={() => deleteCallback(etapiTokenId!, name)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -86,7 +86,7 @@ export function SyncTest() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.showMessage(result.message);
|
toast.showMessage(result.message);
|
||||||
} else {
|
} else {
|
||||||
toast.showError(t("sync_2.handshake_failed", { message: result.error }));
|
toast.showError(t("sync_2.handshake_failed", { message: result.message }));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import etapiTokenService from "../../services/etapi_tokens.js";
|
import etapiTokenService from "../../services/etapi_tokens.js";
|
||||||
|
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
function getTokens() {
|
function getTokens() {
|
||||||
const tokens = etapiTokenService.getTokens();
|
const tokens = etapiTokenService.getTokens();
|
||||||
|
|
||||||
tokens.sort((a, b) => (a.utcDateCreated < b.utcDateCreated ? -1 : 1));
|
tokens.sort((a, b) => (a.utcDateCreated < b.utcDateCreated ? -1 : 1));
|
||||||
|
|
||||||
return tokens;
|
return tokens satisfies EtapiToken[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToken(req: Request) {
|
function createToken(req: Request) {
|
||||||
return etapiTokenService.createToken(req.body.tokenName);
|
return etapiTokenService.createToken(req.body.tokenName) satisfies PostTokensResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchToken(req: Request) {
|
function patchToken(req: Request) {
|
||||||
|
@ -35,7 +35,7 @@ async function testSync(): Promise<SyncTestResponse> {
|
|||||||
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errMessage
|
message: errMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
import { AttributeRow, NoteType } from "./rows.js";
|
import { AttributeRow, NoteType } from "./rows.js";
|
||||||
|
|
||||||
|
type Response = {
|
||||||
|
success: true,
|
||||||
|
message: string;
|
||||||
|
} | {
|
||||||
|
success: false;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppInfo {
|
export interface AppInfo {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
dbVersion: number;
|
dbVersion: number;
|
||||||
@ -78,10 +86,14 @@ export interface AnonymizedDbResponse {
|
|||||||
fileName: string;
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncTestResponse = {
|
export type SyncTestResponse = Response;
|
||||||
success: true;
|
|
||||||
message: string;
|
export interface EtapiToken {
|
||||||
} | {
|
name: string;
|
||||||
success: false;
|
utcDateCreated: string;
|
||||||
error: string;
|
etapiTokenId?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface PostTokensResponse {
|
||||||
|
authToken: string;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user