summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorおさむのひと <46447427+samunohito@users.noreply.github.com>2025-01-14 19:57:58 +0900
committerGitHub <noreply@github.com>2025-01-14 10:57:58 +0000
commit64501c69a10323067dee739790b5a4fc5104e50d (patch)
tree7ab362a252624592e70e5b9a5d4977d4bde3b05c /packages/frontend/src/components
parentApPersonServiceとApNoteServiceのuri <-> url比較を緩和 (#15233) (diff)
downloadmisskey-64501c69a10323067dee739790b5a4fc5104e50d.tar.gz
misskey-64501c69a10323067dee739790b5a4fc5104e50d.tar.bz2
misskey-64501c69a10323067dee739790b5a4fc5104e50d.zip
feat(frontend): Botプロテクションの設定変更時は実際に検証を通過しないと保存できないようにする (#15151)
* feat(frontend): CAPTCHAの設定変更時は実際に検証を通過しないと保存できないようにする * なしでも保存できるようにした * fix CHANGELOG.md * フォームが増殖するのを修正 * add comment * add server-side verify * fix ci * fix * fix * fix i18n * add current.ts * fix text * fix * regenerate locales * fix MkFormFooter.vue --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue64
-rw-r--r--packages/frontend/src/components/MkFormFooter.vue9
2 files changed, 65 insertions, 8 deletions
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 264cf9af06..b1167bbac6 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount
import { defaultStore } from '@/store.js';
// APIs provided by Captcha services
+// see: https://docs.hcaptcha.com/configuration/#javascript-api
+// see: https://developers.google.com/recaptcha/docs/display?hl=ja
+// see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
export type Captcha = {
render(container: string | Node, options: {
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
@@ -53,6 +56,7 @@ declare global {
const props = defineProps<{
provider: CaptchaProvider;
sitekey: string | null; // null will show error on request
+ secretKey?: string | null;
instanceUrl?: string | null;
modelValue?: string | null;
}>();
@@ -64,7 +68,7 @@ const emit = defineEmits<{
const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
-
+const captchaWidgetId = ref<string | undefined>(undefined);
const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false);
@@ -94,6 +98,15 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
+watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
+ // 変更があったときはリフレッシュと再レンダリングをしておかないと、変更後の値で再検証が出来ない
+ if (available.value) {
+ callback(undefined);
+ clearWidget();
+ await requestRender();
+ }
+});
+
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
@@ -106,14 +119,38 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha')
}
function reset() {
- if (captcha.value.reset) captcha.value.reset();
+ if (captcha.value.reset && captchaWidgetId.value !== undefined) {
+ try {
+ captcha.value.reset(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
testcaptchaPassed.value = false;
testcaptchaInput.value = '';
}
+function remove() {
+ if (captcha.value.remove && captchaWidgetId.value) {
+ try {
+ if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value);
+ captcha.value.remove(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
+}
+
async function requestRender() {
- if (captcha.value.render && captchaEl.value instanceof Element) {
- captcha.value.render(captchaEl.value, {
+ if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
+ // reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
+ // (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
+ const elem = document.createElement('div');
+ captchaEl.value.appendChild(elem);
+
+ captchaWidgetId.value = captcha.value.render(elem, {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
@@ -133,6 +170,23 @@ async function requestRender() {
}
}
+function clearWidget() {
+ if (props.provider === 'mcaptcha') {
+ const container = document.getElementById('mcaptcha__widget-container');
+ if (container) {
+ container.innerHTML = '';
+ }
+ } else {
+ reset();
+ remove();
+
+ if (captchaEl.value) {
+ // レンダリング先のコンテナの中身を掃除し、フォームが増殖するのを抑止
+ captchaEl.value.innerHTML = '';
+ }
+ }
+}
+
function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null);
}
@@ -165,7 +219,7 @@ onUnmounted(() => {
});
onBeforeUnmount(() => {
- reset();
+ clearWidget();
});
defineExpose({
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
index f409f6ce50..96214a9542 100644
--- a/packages/frontend/src/components/MkFormFooter.vue
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
<div style="margin-left: auto;" class="_buttons">
<MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
- <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton primary rounded :disabled="!canSaving" @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</template>
@@ -18,7 +18,7 @@ import { } from 'vue';
import MkButton from './MkButton.vue';
import { i18n } from '@/i18n.js';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
form: {
modifiedCount: {
value: number;
@@ -26,7 +26,10 @@ const props = defineProps<{
discard: () => void;
save: () => void;
};
-}>();
+ canSaving?: boolean;
+}>(), {
+ canSaving: true,
+});
</script>
<style lang="scss" module>