summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-10-25 14:20:33 +0900
committerGitHub <noreply@github.com>2024-10-25 14:20:33 +0900
commit076cc953e2bcd9f7335e2d9799cdf902829816cb (patch)
tree03043ce19df2a87708b0edc4d50700a639c41ed6 /packages
parentenhance(frontend): 「単なるラッキー」の調整 (#14807) (diff)
downloadsharkey-076cc953e2bcd9f7335e2d9799cdf902829816cb.tar.gz
sharkey-076cc953e2bcd9f7335e2d9799cdf902829816cb.tar.bz2
sharkey-076cc953e2bcd9f7335e2d9799cdf902829816cb.zip
enhance(frontend): 外部アプリ認証画面の改良 (#14828)
* enhance(frontend): 外部アプリ認証画面の改良 * :art: * lint * Update Changelog * indent * lint * enhance: miauthのリダイレクト先をUI内でも表示するように * :art: * fix * fix
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/_boot_.ts2
-rw-r--r--packages/frontend/src/account.ts70
-rw-r--r--packages/frontend/src/components/MkAuthConfirm.stories.impl.ts7
-rw-r--r--packages/frontend/src/components/MkAuthConfirm.vue450
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue10
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue10
-rw-r--r--packages/frontend/src/pages/miauth.vue145
-rw-r--r--packages/frontend/src/pages/oauth.vue111
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue20
9 files changed, 674 insertions, 151 deletions
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index 13a97e433c..c90cc6bdd0 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -12,7 +12,7 @@ import '@/style.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
-const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
+const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete'];
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
subBoot();
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index f5a74a0581..36186ecac1 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -235,26 +235,6 @@ export async function openAccountMenu(opts: {
}, ev: MouseEvent) {
if (!$i) return;
- function showSigninDialog() {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
- addAccount(res.id, res.i);
- success();
- },
- closed: () => dispose(),
- });
- }
-
- function createAccount() {
- const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: (res: Misskey.entities.SignupResponse) => {
- addAccount(res.id, res.token);
- switchAccountWithToken(res.token);
- },
- closed: () => dispose(),
- });
- }
-
async function switchAccount(account: Misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const found = storedAccounts.find(x => x.id === account.id);
@@ -323,10 +303,22 @@ export async function openAccountMenu(opts: {
text: i18n.ts.addAccount,
children: [{
text: i18n.ts.existingAccount,
- action: () => { showSigninDialog(); },
+ action: () => {
+ getAccountWithSigninDialog().then(res => {
+ if (res != null) {
+ success();
+ }
+ });
+ },
}, {
text: i18n.ts.createAccount,
- action: () => { createAccount(); },
+ action: () => {
+ getAccountWithSignupDialog().then(res => {
+ if (res != null) {
+ switchAccountWithToken(res.token);
+ }
+ });
+ },
}],
}, {
type: 'link',
@@ -347,6 +339,40 @@ export async function openAccountMenu(opts: {
});
}
+export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+ return new Promise((resolve) => {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+ done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+ await addAccount(res.id, res.i);
+ resolve({ id: res.id, token: res.i });
+ },
+ cancelled: () => {
+ resolve(null);
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ });
+}
+
+export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
+ return new Promise((resolve) => {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+ done: async (res: Misskey.entities.SignupResponse) => {
+ await addAccount(res.id, res.token);
+ resolve({ id: res.id, token: res.token });
+ },
+ cancelled: () => {
+ resolve(null);
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ });
+}
+
if (_DEV_) {
(window as any).$i = $i;
}
diff --git a/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
new file mode 100644
index 0000000000..0adc44e204
--- /dev/null
+++ b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkAuthConfirm from './MkAuthConfirm.vue';
+void MkAuthConfirm;
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
new file mode 100644
index 0000000000..f5f6d7f6cc
--- /dev/null
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -0,0 +1,450 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+ <Transition
+ mode="out-in"
+ :enterActiveClass="$style.transition_enterActive"
+ :leaveActiveClass="$style.transition_leaveActive"
+ :enterFromClass="$style.transition_enterFrom"
+ :leaveToClass="$style.transition_leaveTo"
+
+ :inert="_waiting"
+ >
+ <div v-if="phase === 'accountSelect'" key="accountSelect" :class="$style.root" class="_gaps">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-user"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div>
+ </div>
+ <div :class="$style.accountSelectorRoot">
+ <div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div>
+ <div :class="$style.accountSelectorList">
+ <template v-for="[id, user] in users">
+ <input :id="'account-' + id" v-model="selectedUser" type="radio" name="accountSelector" :value="id" :class="$style.accountSelectorRadio"/>
+ <label :for="'account-' + id" :class="$style.accountSelectorItem">
+ <MkAvatar :user="user" :class="$style.accountSelectorAvatar"/>
+ <div :class="$style.accountSelectorBody">
+ <MkUserName :user="user" :class="$style.accountSelectorName"/>
+ <MkAcct :user="user" :class="$style.accountSelectorAcct"/>
+ </div>
+ </label>
+ </template>
+ <button class="_button" :class="[$style.accountSelectorItem, $style.accountSelectorAddAccountRoot]" @click="clickAddAccount">
+ <div :class="[$style.accountSelectorAvatar, $style.accountSelectorAddAccountAvatar]">
+ <i class="ti ti-user-plus"></i>
+ </div>
+ <div :class="[$style.accountSelectorBody, $style.accountSelectorName]">{{ i18n.ts.addAccount }}</div>
+ </button>
+ </div>
+ </div>
+ <div class="_buttonsCenter">
+ <MkButton rounded gradate :disabled="selectedUser === null" @click="clickChooseAccount">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </div>
+ <div v-else-if="phase === 'consent'" key="consent" :class="$style.root" class="_gaps">
+ <div :class="$style.header" class="_gaps_s">
+ <img v-if="icon" :class="$style.icon" :src="getProxiedImageUrl(icon, 'preview')"/>
+ <div v-else :class="$style.iconFallback">
+ <i class="ti ti-apps"></i>
+ </div>
+ <div :class="$style.headerText">{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}</div>
+ </div>
+ <div v-if="permissions && permissions.length > 0" class="_gaps_s" :class="$style.permissionRoot">
+ <div>{{ name ? i18n.tsx._auth.permission({ name }) : i18n.ts._auth.permissionAsk }}</div>
+ <div :class="$style.permissionListWrapper">
+ <ul :class="$style.permissionList">
+ <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
+ </ul>
+ </div>
+ </div>
+ <slot name="consentAdditionalInfo"></slot>
+ <div :class="$style.accountSelectorRoot">
+ <div :class="$style.accountSelectorLabel">
+ {{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button>
+ </div>
+ <div :class="$style.accountSelectorList">
+ <div :class="[$style.accountSelectorItem, $style.static]">
+ <MkAvatar :user="users.get(selectedUser!)!" :class="$style.accountSelectorAvatar"/>
+ <div :class="$style.accountSelectorBody">
+ <MkUserName :user="users.get(selectedUser!)!" :class="$style.accountSelectorName"/>
+ <MkAcct :user="users.get(selectedUser!)!" :class="$style.accountSelectorAcct"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="_buttonsCenter">
+ <MkButton rounded @click="clickCancel">{{ i18n.ts.reject }}</MkButton>
+ <MkButton rounded gradate @click="clickAccept">{{ i18n.ts.accept }}</MkButton>
+ </div>
+ </div>
+ <div v-else-if="phase === 'success'" key="success" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-check"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts._auth.accepted }}</div>
+ <div :class="$style.headerTextSub">{{ i18n.ts._auth.pleaseGoBack }}</div>
+ </div>
+ </div>
+ <div v-else-if="phase === 'denied'" key="denied" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-x"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts._auth.denied }}</div>
+ </div>
+ </div>
+ <div v-else-if="phase === 'failed'" key="failed" :class="$style.root" class="_gaps_s">
+ <div :class="$style.header" class="_gaps_s">
+ <div :class="$style.iconFallback">
+ <i class="ti ti-x"></i>
+ </div>
+ <div :class="$style.headerText">{{ i18n.ts.somethingHappened }}</div>
+ </div>
+ </div>
+ </Transition>
+ <div v-if="_waiting" :class="$style.waitingRoot">
+ <MkLoading/>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+
+const props = defineProps<{
+ name?: string;
+ icon?: string;
+ permissions?: (typeof Misskey.permissions[number])[];
+ manualWaiting?: boolean;
+ waitOnDeny?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'accept', token: string): void;
+ (ev: 'deny', token: string): void;
+}>();
+
+const waiting = ref(true);
+const _waiting = computed(() => waiting.value || props.manualWaiting);
+const phase = ref<'accountSelect' | 'consent' | 'success' | 'denied' | 'failed'>('accountSelect');
+
+const selectedUser = ref<string | null>(null);
+
+const users = ref(new Map<string, Misskey.entities.UserDetailed & { token: string; }>());
+
+async function init() {
+ waiting.value = true;
+
+ users.value.clear();
+
+ if ($i) {
+ users.value.set($i.id, $i);
+ }
+
+ const accounts = await getAccounts();
+
+ const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
+
+ if (accountIdsToFetch.length > 0) {
+ const usersRes = await misskeyApi('users/show', {
+ userIds: accountIdsToFetch,
+ });
+
+ for (const user of usersRes) {
+ if (users.value.has(user.id)) continue;
+
+ users.value.set(user.id, {
+ ...user,
+ token: accounts.find(a => a.id === user.id)!.token,
+ });
+ }
+ }
+
+ waiting.value = false;
+}
+
+init();
+
+function clickAddAccount(ev: MouseEvent) {
+ selectedUser.value = null;
+
+ os.popupMenu([{
+ text: i18n.ts.existingAccount,
+ action: () => {
+ getAccountWithSigninDialog().then(async (res) => {
+ if (res != null) {
+ os.success();
+ await init();
+ if (users.value.has(res.id)) {
+ selectedUser.value = res.id;
+ }
+ }
+ });
+ },
+ }, {
+ text: i18n.ts.createAccount,
+ action: () => {
+ getAccountWithSignupDialog().then(async (res) => {
+ if (res != null) {
+ os.success();
+ await init();
+ if (users.value.has(res.id)) {
+ selectedUser.value = res.id;
+ }
+ }
+ });
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
+function clickChooseAccount() {
+ if (selectedUser.value === null) return;
+
+ phase.value = 'consent';
+}
+
+function clickBackToAccountSelect() {
+ selectedUser.value = null;
+ phase.value = 'accountSelect';
+}
+
+function clickCancel() {
+ if (selectedUser.value === null) return;
+
+ const user = users.value.get(selectedUser.value)!;
+
+ const token = user.token;
+
+ if (props.waitOnDeny) {
+ waiting.value = true;
+ }
+ emit('deny', token);
+}
+
+async function clickAccept() {
+ if (selectedUser.value === null) return;
+
+ const user = users.value.get(selectedUser.value)!;
+
+ const token = user.token;
+
+ waiting.value = true;
+ emit('accept', token);
+}
+
+function showUI(state: 'success' | 'denied' | 'failed') {
+ phase.value = state;
+ waiting.value = false;
+}
+
+defineExpose({
+ showUI,
+});
+</script>
+
+<style lang="scss" module>
+.transition_enterActive,
+.transition_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_enterFrom {
+ opacity: 0;
+ transform: translateX(50px);
+}
+.transition_leaveTo {
+ opacity: 0;
+ transform: translateX(-50px);
+}
+
+.wrapper {
+ overflow-x: hidden;
+ overflow-x: clip;
+
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.waitingRoot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1;
+ cursor: wait;
+}
+
+.root {
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 48px 24px;
+}
+
+.header {
+ margin: 0 auto;
+ max-width: 320px;
+}
+
+.icon,
+.iconFallback {
+ display: block;
+ margin: 0 auto;
+ width: 54px;
+ height: 54px;
+}
+
+.icon {
+ border-radius: 50%;
+ border: 1px solid var(--MI_THEME-divider);
+ background-color: #fff;
+ object-fit: contain;
+}
+
+.iconFallback {
+ border-radius: 50%;
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ text-align: center;
+ line-height: 54px;
+ font-size: 18px;
+}
+
+.headerText,
+.headerTextSub {
+ text-align: center;
+ word-break: normal;
+ word-break: auto-phrase;
+}
+
+.headerText {
+ font-size: 16px;
+ font-weight: 700;
+}
+
+.permissionRoot {
+ padding: 16px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-bg);
+}
+
+.permissionListWrapper {
+ max-height: 350px;
+ overflow-y: auto;
+ padding: 12px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+}
+
+.permissionList {
+ margin: 0 0 0 1.5em;
+ padding: 0;
+ font-size: 90%;
+}
+
+.accountSelectorLabel {
+ font-size: 0.85em;
+ opacity: 0.7;
+ margin-bottom: 8px;
+}
+
+.accountSelectorList {
+ border-radius: var(--MI-radius);
+ border: 1px solid var(--MI_THEME-divider);
+ overflow: hidden;
+ overflow: clip;
+}
+
+.accountSelectorRadio {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+
+ &:focus-visible + .accountSelectorItem {
+ outline: 2px solid var(--MI_THEME-accent);
+ outline-offset: -4px;
+ }
+
+ &:checked:focus-visible + .accountSelectorItem {
+ outline-color: #fff;
+ }
+
+ &:checked + .accountSelectorItem {
+ background: var(--MI_THEME-accent);
+ color: #fff;
+ }
+}
+
+.accountSelectorItem {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ font-size: 14px;
+ -webkit-tap-highlight-color: transparent;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--MI_THEME-buttonHoverBg);
+ }
+
+ &.static {
+ cursor: unset;
+
+ &:hover {
+ background: none;
+ }
+ }
+}
+
+.accountSelectorAddAccountRoot {
+ width: 100%;
+}
+
+.accountSelectorBody {
+ padding: 0 8px;
+ min-width: 0;
+}
+
+.accountSelectorAvatar {
+ width: 45px;
+ height: 45px;
+}
+
+.accountSelectorAddAccountAvatar {
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ font-size: 16px;
+ line-height: 45px;
+ text-align: center;
+ border-radius: 50%;
+}
+
+.accountSelectorName {
+ display: block;
+ font-weight: bold;
+}
+
+.accountSelectorAcct {
+ opacity: 0.5;
+}
+</style>
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index fe9e1ce088..f06cfffee4 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -26,11 +26,11 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
- withOkButton: boolean;
- withCloseButton: boolean;
- okButtonDisabled: boolean;
- width: number;
- height: number;
+ withOkButton?: boolean;
+ withCloseButton?: boolean;
+ okButtonDisabled?: boolean;
+ width?: number;
+ height?: number;
}>(), {
withOkButton: false,
withCloseButton: true,
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index f240e6dc46..4f75a36fbe 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="500"
:height="600"
- @close="dialog?.close()"
+ @close="onClose"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.signup }}</template>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="!isAcceptedServerRule">
- <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
+ <XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
</template>
<template v-else>
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'done', res: Misskey.entities.SignupResponse): void;
+ (ev: 'cancelled'): void;
(ev: 'closed'): void;
}>();
@@ -55,6 +56,11 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
+function onClose() {
+ emit('cancelled');
+ dialog.value?.close();
+}
+
function onSignup(res: Misskey.entities.SignupResponse) {
emit('done', res);
dialog.value?.close();
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ffaf739ed0..283f66ac45 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkStickyContainer>
- <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :contentMax="800">
- <div v-if="$i">
- <div v-if="state == 'waiting'">
- <MkLoading/>
- </div>
- <div v-if="state == 'denied'">
- <p>{{ i18n.ts._auth.denied }}</p>
- </div>
- <div v-else-if="state == 'accepted'" class="accepted">
- <p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
- <p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
- </div>
- <div v-else>
- <div v-if="_permissions.length > 0">
- <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
- <p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
- <ul>
- <li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
- </ul>
- </div>
- <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
- <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
- <div :class="$style.buttons">
- <MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
- <MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
- </div>
- </div>
+<div>
+ <MkAnimBg style="position: fixed; top: 0;"/>
+ <div :class="$style.formContainer">
+ <div :class="$style.form">
+ <MkAuthConfirm
+ ref="authRoot"
+ :name="name"
+ :icon="icon || undefined"
+ :permissions="_permissions"
+ @accept="onAccept"
+ @deny="onDeny"
+ >
+ <template #consentAdditionalInfo>
+ <div v-if="callback != null" :class="$style.redirectRoot">
+ <div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div>
+ <div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div>
+ </div>
+ </template>
+ </MkAuthConfirm>
</div>
- <div v-else>
- <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
- <MkSignin @login="onLogin"/>
- </div>
- </MkSpacer>
-</MkStickyContainer>
+ </div>
+</div>
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { computed, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkAnimBg from '@/components/MkAnimBg.vue';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
+
import { i18n } from '@/i18n.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
session: string;
callback?: string;
- name: string;
- icon: string;
- permission: string; // コンマ区切り
+ name?: string;
+ icon?: string;
+ permission?: string; // コンマ区切り
}>();
-const _permissions = props.permission ? props.permission.split(',') : [];
+const _permissions = computed(() => {
+ return (props.permission ? props.permission.split(',').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) : []);
+});
-const state = ref<string | null>(null);
+const authRoot = useTemplateRef('authRoot');
-async function accept(): Promise<void> {
- state.value = 'waiting';
+async function onAccept(token: string) {
await misskeyApi('miauth/gen-token', {
session: props.session,
name: props.name,
iconUrl: props.icon,
- permission: _permissions,
+ permission: _permissions.value,
+ }, token).catch(() => {
+ authRoot.value?.showUI('failed');
});
- state.value = 'accepted';
- if (props.callback) {
+ if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session);
- location.href = cbUrl.href;
+ location.href = cbUrl.toString();
+ } else {
+ authRoot.value?.showUI('success');
}
}
-function deny(): void {
- state.value = 'denied';
-}
-
-function onLogin(res): void {
- login(res.i);
+function onDeny() {
+ authRoot.value?.showUI('denied');
}
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
definePageMetadata(() => ({
title: 'MiAuth',
icon: 'ti ti-apps',
@@ -100,15 +84,38 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.buttons {
- margin-top: 16px;
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
+.formContainer {
+ min-height: 100svh;
+ padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+ box-sizing: border-box;
+ display: grid;
+ place-content: center;
+}
+
+.form {
+ position: relative;
+ z-index: 10;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ overflow: clip;
+ max-width: 500px;
+ width: calc(100vw - 64px);
+ height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+ overflow-y: scroll;
+}
+
+.redirectRoot {
+ padding: 16px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-bg);
}
-.loginMessage {
- text-align: center;
- margin: 8px 0 24px;
+.redirectUrl {
+ font-size: 90%;
+ padding: 12px;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ overflow-x: scroll;
}
</style>
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 733e34eb2c..8719a769e5 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkStickyContainer>
- <template #header><MkPageHeader/></template>
- <MkSpacer :contentMax="800">
- <div v-if="$i">
- <div v-if="permissions.length > 0">
- <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
- <p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
- <ul>
- <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
- </ul>
- </div>
- <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
- <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
- <form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
- <input name="login_token" type="hidden" :value="$i.token"/>
- <input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/>
- <MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
- <MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
- </form>
+<div>
+ <MkAnimBg style="position: fixed; top: 0;"/>
+ <div :class="$style.formContainer">
+ <div :class="$style.form">
+ <MkAuthConfirm
+ ref="authRoot"
+ :name="name"
+ :permissions="permissions"
+ :waitOnDeny="true"
+ @accept="onAccept"
+ @deny="onDeny"
+ />
</div>
- <div v-else>
- <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
- <MkSignin @login="onLogin"/>
- </div>
- </MkSpacer>
-</MkStickyContainer>
+ </div>
+</div>
</template>
<script lang="ts" setup>
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { $i, login } from '@/account.js';
-import { i18n } from '@/i18n.js';
+import * as Misskey from 'misskey-js';
+import MkAnimBg from '@/components/MkAnimBg.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
if (transactionIdMeta) {
@@ -45,10 +33,44 @@ if (transactionIdMeta) {
}
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
-const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? [];
+const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
+
+function doPost(token: string, decision: 'accept' | 'deny') {
+ const form = document.createElement('form');
+ form.action = '/oauth/decision';
+ form.method = 'post';
+ form.acceptCharset = 'utf-8';
+
+ const loginToken = document.createElement('input');
+ loginToken.type = 'hidden';
+ loginToken.name = 'login_token';
+ loginToken.value = token;
+ form.appendChild(loginToken);
+
+ const transactionId = document.createElement('input');
+ transactionId.type = 'hidden';
+ transactionId.name = 'transaction_id';
+ transactionId.value = transactionIdMeta?.content ?? '';
+ form.appendChild(transactionId);
+
+ if (decision === 'deny') {
+ const cancel = document.createElement('input');
+ cancel.type = 'hidden';
+ cancel.name = 'cancel';
+ cancel.value = 'cancel';
+ form.appendChild(cancel);
+ }
+
+ document.body.appendChild(form);
+ form.submit();
+}
+
+function onAccept(token: string) {
+ doPost(token, 'accept');
+}
-function onLogin(res): void {
- login(res.i);
+function onDeny(token: string) {
+ doPost(token, 'deny');
}
definePageMetadata(() => ({
@@ -58,15 +80,24 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.buttons {
- margin-top: 16px;
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
+.formContainer {
+ min-height: 100svh;
+ padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+ box-sizing: border-box;
+ display: grid;
+ place-content: center;
}
-.loginMessage {
- text-align: center;
- margin: 8px 0 24px;
+.form {
+ position: relative;
+ z-index: 10;
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ overflow: clip;
+ max-width: 500px;
+ width: calc(100vw - 64px);
+ height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+ overflow-y: scroll;
}
</style>
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 1bbedb817e..16f0716a12 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref, computed } from 'vue';
+import { ref, computed } from 'vue';
import type * as Misskey from 'misskey-js';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js';
+import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -74,23 +74,19 @@ async function removeAccount(account: Misskey.entities.UserDetailed) {
}
function addExistingAccount() {
- const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
- await addAccounts(res.id, res.i);
+ getAccountWithSigninDialog().then((res) => {
+ if (res != null) {
os.success();
init();
- },
- closed: () => dispose(),
+ }
});
}
function createAccount() {
- const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: async (res: Misskey.entities.SignupResponse) => {
- await addAccounts(res.id, res.token);
+ getAccountWithSignupDialog().then((res) => {
+ if (res != null) {
switchAccountWithToken(res.token);
- },
- closed: () => dispose(),
+ }
});
}