mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
Add PUT /admin/api/groups/:id/rename (owner role) which re-points the group's routes, migrates the per-group webhook secret (tenant:{id}) and pending invites, and records an audit entry. The group editor's id field is now editable and the console renames first, then persists remaining edits under the new id.
315 lines
10 KiB
Vue
315 lines
10 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition name="fade">
|
|
<div v-if="open" class="overlay" @click.self="close"></div>
|
|
</Transition>
|
|
<Transition name="slide">
|
|
<aside v-if="open" class="editor" role="dialog" aria-modal="true">
|
|
<div class="editor-head">
|
|
<h2>{{ isEdit ? t("groupEditor.editTitle") : t("groupEditor.newTitle") }}</h2>
|
|
<button class="icon-btn" :title="t('groupEditor.close')" @click="close">✕</button>
|
|
</div>
|
|
<form class="editor-body" @submit.prevent="save">
|
|
<div class="row2">
|
|
<div class="field">
|
|
<label>{{ t("groupEditor.name") }}</label>
|
|
<input
|
|
v-model="form.name"
|
|
type="text"
|
|
:placeholder="t('groupEditor.namePlaceholder')"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ t("groupEditor.id") }}</label>
|
|
<input
|
|
v-model="form.id"
|
|
type="text"
|
|
:placeholder="t('groupEditor.idPlaceholder')"
|
|
required
|
|
/>
|
|
<div class="hint">
|
|
{{ isEdit ? t("groupEditor.renameHint") : t("groupEditor.idHint") }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row2">
|
|
<div class="field">
|
|
<label>{{ t("groupEditor.language") }}</label>
|
|
<input
|
|
v-model="form.lang"
|
|
type="text"
|
|
:placeholder="t('groupEditor.langPlaceholder')"
|
|
/>
|
|
<div class="hint">{{ t("groupEditor.langHint") }}</div>
|
|
</div>
|
|
<div class="field">
|
|
<label
|
|
>{{ t("groupEditor.emoji") }}
|
|
<span class="lbl-note">{{ t("groupEditor.emojiNote") }}</span></label
|
|
>
|
|
<label class="inline">
|
|
<input v-model="form.emoji" type="checkbox" />
|
|
<span>{{ t("groupEditor.emojiLabel") }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label
|
|
>{{ t("groupEditor.membersNote") }}
|
|
<span class="lbl-note">{{ t("groupEditor.membersHint") }}</span></label
|
|
>
|
|
<p class="hint">
|
|
{{ t("groupEditor.membersGoPanel") }}
|
|
</p>
|
|
</div>
|
|
<div v-if="superAdmin" class="field">
|
|
<label
|
|
>{{ t("groupEditor.owners") }}
|
|
<span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label
|
|
>
|
|
<input
|
|
v-model="form.owners"
|
|
type="text"
|
|
:placeholder="t('groupEditor.ownersPlaceholder')"
|
|
/>
|
|
<div class="hint">{{ t("groupEditor.ownersHint") }}</div>
|
|
</div>
|
|
<div v-else class="field">
|
|
<label
|
|
>{{ t("groupEditor.owners") }}
|
|
<span class="lbl-note">{{ t("groupEditor.ownersSuperOnly") }}</span></label
|
|
>
|
|
<input v-model="ownersReadonly" type="text" disabled />
|
|
</div>
|
|
<div class="field">
|
|
<label
|
|
>{{ t("groupEditor.providers") }}
|
|
<span class="lbl-note">{{ t("groupEditor.providersNote") }}</span></label
|
|
>
|
|
<div class="provider-options">
|
|
<label class="inline">
|
|
<input
|
|
type="checkbox"
|
|
:checked="form.providers.includes('github')"
|
|
@change="toggleProvider('github', $event)"
|
|
/>
|
|
<span>GitHub</span>
|
|
</label>
|
|
<label class="inline">
|
|
<input
|
|
type="checkbox"
|
|
:checked="form.providers.includes('gitea')"
|
|
@change="toggleProvider('gitea', $event)"
|
|
/>
|
|
<span>Gitea</span>
|
|
</label>
|
|
</div>
|
|
<div class="hint">{{ t("groupEditor.providersHint") }}</div>
|
|
</div>
|
|
<div class="field">
|
|
<label
|
|
>{{ t("groupEditor.installationId") }}
|
|
<span class="lbl-note">{{ t("groupEditor.installationIdNote") }}</span></label
|
|
>
|
|
<input
|
|
v-model="form.installationId"
|
|
type="text"
|
|
inputmode="numeric"
|
|
:placeholder="t('groupEditor.installationIdPlaceholder')"
|
|
/>
|
|
<div class="hint">{{ t("groupEditor.installationIdHint") }}</div>
|
|
</div>
|
|
<div class="field">
|
|
<label
|
|
>{{ t("groupEditor.logTarget") }}
|
|
<span class="lbl-note">{{ t("groupEditor.logTargetNote") }}</span></label
|
|
>
|
|
<select v-model="form.logPlatform">
|
|
<option value="">{{ t("groupEditor.logDisabled") }}</option>
|
|
<option value="discord">Discord</option>
|
|
<option value="telegram">Telegram</option>
|
|
</select>
|
|
<template v-if="form.logPlatform === 'discord'">
|
|
<input
|
|
v-model="form.logChannelId"
|
|
type="text"
|
|
:placeholder="t('routeEditor.channelPlaceholder')"
|
|
/>
|
|
<input
|
|
v-model="form.logThreadId"
|
|
type="text"
|
|
:placeholder="t('routeEditor.threadPlaceholder')"
|
|
/>
|
|
</template>
|
|
<template v-else-if="form.logPlatform === 'telegram'">
|
|
<input
|
|
v-model="form.logChatId"
|
|
type="text"
|
|
:placeholder="t('routeEditor.chatPlaceholder')"
|
|
/>
|
|
<input
|
|
v-model="form.logTopicId"
|
|
type="text"
|
|
:placeholder="t('routeEditor.topicPlaceholder')"
|
|
/>
|
|
</template>
|
|
<div class="hint">{{ t("groupEditor.logTargetHint") }}</div>
|
|
</div>
|
|
<div class="err">{{ formError }}</div>
|
|
</form>
|
|
<div class="editor-foot">
|
|
<button class="btn btn-ghost" type="button" @click="close">
|
|
{{ t("groupEditor.cancel") }}
|
|
</button>
|
|
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
|
|
{{ t("groupEditor.save") }}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, watch } from "vue";
|
|
import type { Group } from "~/types";
|
|
|
|
const { t } = useI18n();
|
|
|
|
const props = defineProps<{
|
|
open: boolean;
|
|
group: Group | null;
|
|
saving: boolean;
|
|
superAdmin?: boolean;
|
|
}>();
|
|
const emit = defineEmits<{
|
|
(e: "close"): void;
|
|
(e: "save", group: Group): void;
|
|
}>();
|
|
|
|
const isEdit = computed(() => props.group != null);
|
|
const formError = ref("");
|
|
const ownersReadonly = computed(() => (props.group?.owners ?? []).join(", "));
|
|
|
|
const form = reactive({
|
|
id: "",
|
|
name: "",
|
|
owners: "",
|
|
providers: [] as ("github" | "gitea")[],
|
|
installationId: "",
|
|
emoji: true,
|
|
lang: "",
|
|
logPlatform: "" as "" | "discord" | "telegram",
|
|
logChannelId: "",
|
|
logThreadId: "",
|
|
logChatId: "",
|
|
logTopicId: "",
|
|
});
|
|
|
|
function splitList(text: string): string[] {
|
|
return text
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function toggleProvider(p: "github" | "gitea", e: Event): void {
|
|
const checked = (e.target as HTMLInputElement).checked;
|
|
form.providers = checked
|
|
? [...new Set([...form.providers, p])]
|
|
: form.providers.filter((x) => x !== p);
|
|
}
|
|
|
|
watch(
|
|
() => props.open,
|
|
(open) => {
|
|
if (!open) return;
|
|
const g = props.group;
|
|
const lt = g?.logTarget;
|
|
form.id = g?.id ?? "";
|
|
form.name = g?.name ?? "";
|
|
form.owners = (g?.owners ?? []).join(", ");
|
|
form.providers = (g?.providers ?? []).filter(
|
|
(p): p is "github" | "gitea" => p === "github" || p === "gitea",
|
|
);
|
|
form.installationId = g?.installationId != null ? String(g.installationId) : "";
|
|
form.emoji = g?.emoji ?? true;
|
|
form.lang = g?.lang ?? "";
|
|
form.logPlatform = lt?.platform ?? "";
|
|
form.logChannelId = lt?.channelId ?? "";
|
|
form.logThreadId = lt?.threadId ?? "";
|
|
form.logChatId = lt?.chatId ?? "";
|
|
form.logTopicId = lt?.topicId ?? "";
|
|
formError.value = "";
|
|
},
|
|
);
|
|
|
|
function close(): void {
|
|
emit("close");
|
|
}
|
|
|
|
function save(): void {
|
|
formError.value = "";
|
|
const id = form.id.trim();
|
|
const name = form.name.trim();
|
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
|
formError.value = t("groupEditor.errIdFormat");
|
|
return;
|
|
}
|
|
if (!name) {
|
|
formError.value = t("groupEditor.errName");
|
|
return;
|
|
}
|
|
let logTarget:
|
|
| { platform: "discord"; channelId: string; threadId?: string }
|
|
| {
|
|
platform: "telegram";
|
|
chatId: string;
|
|
topicId?: string;
|
|
}
|
|
| undefined;
|
|
if (form.logPlatform === "discord") {
|
|
const channelId = form.logChannelId.trim();
|
|
if (!channelId) {
|
|
formError.value = t("groupEditor.errLogChannel");
|
|
return;
|
|
}
|
|
logTarget = { platform: "discord", channelId, threadId: form.logThreadId.trim() || undefined };
|
|
} else if (form.logPlatform === "telegram") {
|
|
const chatId = form.logChatId.trim();
|
|
if (!chatId) {
|
|
formError.value = t("groupEditor.errLogChat");
|
|
return;
|
|
}
|
|
logTarget = { platform: "telegram", chatId, topicId: form.logTopicId.trim() || undefined };
|
|
}
|
|
const installationText = form.installationId.trim();
|
|
let installationId: number | undefined;
|
|
if (installationText) {
|
|
installationId = Number(installationText);
|
|
if (!Number.isInteger(installationId) || installationId <= 0) {
|
|
formError.value = t("groupEditor.errInstallationId");
|
|
return;
|
|
}
|
|
}
|
|
const owners = splitList(form.owners);
|
|
const members = props.group?.members
|
|
? props.group.members.map((m) => ({ ...m }))
|
|
: props.group?.adminIds?.length
|
|
? props.group.adminIds.map((login) => ({ login, role: "owner" as const }))
|
|
: [];
|
|
emit("save", {
|
|
id,
|
|
name,
|
|
members,
|
|
adminIds: members.filter((m) => m.role === "owner").map((m) => m.login),
|
|
owners: props.superAdmin ? (owners.length ? owners : undefined) : props.group?.owners,
|
|
providers: form.providers.length ? form.providers : undefined,
|
|
installationId,
|
|
emoji: form.emoji,
|
|
lang: form.lang.trim() || undefined,
|
|
logTarget,
|
|
});
|
|
}
|
|
</script>
|