Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance: アカウント削除時のクライアントの挙動をいい感じにするなど #10002

Merged
merged 16 commits into from
Mar 9, 2023
Merged
5 changes: 5 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ flagShowTimelineReplies: "タイムラインにノートへの返信を表示す
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
general: "全般"
Expand Down Expand Up @@ -542,6 +543,10 @@ userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています"
yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
tokenRemoved: "トークンが無効です"
tokenRemovedDescription: "ログイントークンが失効しています。ログインし直してください。"
accountRemoved: "アカウントは削除されています"
accountRemovedDescription: "このアカウントは削除されています。"
menu: "メニュー"
divider: "分割線"
addItem: "項目を追加"
Expand Down
158 changes: 125 additions & 33 deletions packages/frontend/src/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineAsyncComponent, reactive } from 'vue';
import { defineAsyncComponent, reactive, ref } from 'vue';
import * as misskey from 'misskey-js';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
Expand All @@ -25,12 +25,71 @@ export function incNotesCount() {
notesCount++;
}

export async function signout() {
waiting();
miLocalStorage.removeItem('account');
// 複数のダイアログが表示されるのを防止するため、refreshAccountsの同時実行を避ける
const refreshing = ref(false);

// 保存されているアカウント情報全てに対して、最新の情報を取得
export async function refreshAccounts(force = false) {
if (force !== true && refreshing.value) return;
refreshing.value = true;
const storedAccounts = await getAccounts();
const newAccounts: (Account | { id: Account['id']; token: Account['token']; })[] = [];

for (const storedAccount of storedAccounts) {
await fetchAccount(storedAccount.token, storedAccount.id)
.then(res => newAccounts.push(res))
.catch(async reason => {
// サーバーがビジーなどでapi/iが500番台を返した場合にもログアウトされてしまうのは微妙
// しかしアカウント削除は壊れているので、500が返ってくる
// 本当はシンプルに次のようにしたい
// if (reason !== true) newAccounts.push(storedAccount);
if (reason === true) return;
if (reason.text && typeof reason.text === 'function') {
const text = await reason.text();
// `Could not find any entity of type \"UserProfile\" matching`
// と返ってくる場合はアカウントが削除されている
if (text.includes('UserProfile')) {
await alert({
type: 'error',
title: i18n.ts.accountRemoved,
text: i18n.ts.accountRemovedDescription,
});
return;
}
}

newAccounts.push(storedAccount);
});
}

if (newAccounts.length === 0) {
miLocalStorage.removeItem('account');
await del('accounts');
unisonReload('/');
return;
}

set('accounts', newAccounts.map(x => ({ id: x.id, token: x.token })));

if ($i) {
const found = newAccounts.find(x => x.id === $i.id);
if (found) {
updateAccount(found);
} else {
signout(true);
}
}

await removeAccount($i.id);
refreshing.value = false;
return newAccounts;
}

export async function signout(doNotEditAccountList?: boolean) {
if (!$i) return;

waiting();
miLocalStorage.removeItem('account');
if (!doNotEditAccountList) await removeAccount($i.id);
const accounts = await getAccounts();

//#region Remove service worker registration
Expand Down Expand Up @@ -78,13 +137,18 @@ export async function addAccount(id: Account['id'], token: Account['token']) {

export async function removeAccount(id: Account['id']) {
const accounts = await getAccounts();
accounts.splice(accounts.findIndex(x => x.id === id), 1);
const i = accounts.findIndex(x => x.id === id);
if (i !== -1) accounts.splice(i, 1);

if (accounts.length > 0) await set('accounts', accounts);
else await del('accounts');
if (accounts.length > 0) {
await set('accounts', accounts);
await refreshAccounts(true);
} else {
await del('accounts');
}
}

function fetchAccount(token: string): Promise<Account> {
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
return new Promise((done, fail) => {
// Fetch user
window.fetch(`${apiUrl}/i`, {
Expand All @@ -96,44 +160,69 @@ function fetchAccount(token: string): Promise<Account> {
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(res => {
if (res.error) {
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
showSuspendedDialog().then(() => {
signout();
});
} else {
alert({
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
if (res.status >= 500 && res.status < 600) {
// サーバーエラー(5xx)の場合をrejectとする
// (認証エラーなど4xxはresolve)
return fail2(res);
}
res.json().then(done2, fail2);
}))
.then(async res => {
if (res.error) {
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
// SUSPENDED
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
await showSuspendedDialog();
}
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
// AUTHENTICATION_FAILED
// トークンが無効化されていたりアカウントが削除されたりしている
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
await alert({
type: 'error',
title: i18n.ts.failedToFetchAccountInformation,
text: JSON.stringify(res.error),
title: i18n.ts.tokenRemoved,
text: i18n.ts.tokenRemovedDescription,
});
}
} else {
res.token = token;
done(res);
await alert({
type: 'error',
title: i18n.ts.failedToFetchAccountInformation,
text: JSON.stringify(res.error),
});
}
})
.catch(fail);

// rejectかつ理由がtrueの場合、削除対象であることを示す
fail(true);
} else {
(res as Account).token = token;
done(res as Account);
}
}, fail);
});
}

export function updateAccount(accountData) {
export function updateAccount(accountData: Partial<Account>) {
if (!$i) return;
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
}
miLocalStorage.setItem('account', JSON.stringify($i));
}

export function refreshAccount() {
return fetchAccount($i.token).then(updateAccount);
}

export async function login(token: Account['token'], redirect?: string) {
waiting();
const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
success: false,
showing: showing,
}, {}, 'closed');
if (_DEV_) console.log('logging as token ', token);
const me = await fetchAccount(token);
const me = await fetchAccount(token, undefined, true)
.catch(reason => {
showing.value = false;
throw reason;
});
miLocalStorage.setItem('account', JSON.stringify(me));
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
await addAccount(me.id, token);
Expand All @@ -155,6 +244,8 @@ export async function openAccountMenu(opts: {
active?: misskey.entities.UserDetailed['id'];
onChoose?: (account: misskey.entities.UserDetailed) => void;
}, ev: MouseEvent) {
if (!$i) return;

function showSigninDialog() {
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
Expand All @@ -175,8 +266,9 @@ export async function openAccountMenu(opts: {

async function switchAccount(account: misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token;
switchAccountWithToken(token);
const found = storedAccounts.find(x => x.id === account.id);
if (found == null) return;
switchAccountWithToken(found.token);
}

function switchAccountWithToken(token: string) {
Expand Down
8 changes: 4 additions & 4 deletions packages/frontend/src/components/MkSigninDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n';

const props = withDefaults(defineProps<{
withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
}>(), {
Expand All @@ -29,7 +29,7 @@ const props = withDefaults(defineProps<{
});

const emit = defineEmits<{
(ev: 'done'): void;
(ev: 'done', v: any): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
Expand All @@ -38,11 +38,11 @@ const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();

function onClose() {
emit('cancelled');
dialog.close();
if (dialog) dialog.close();
}

function onLogin(res) {
emit('done', res);
dialog.close();
if (dialog) dialog.close();
}
</script>
4 changes: 2 additions & 2 deletions packages/frontend/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { $i, refreshAccounts, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store';
import { fetchInstance, instance } from '@/instance';
import { makeHotkey } from '@/scripts/hotkey';
Expand Down Expand Up @@ -143,7 +143,7 @@ if ($i && $i.token) {
console.log('account cache found. refreshing...');
}

refreshAccount();
refreshAccounts();
} else {
if (_DEV_) {
console.log('no account cache found.');
Expand Down
27 changes: 17 additions & 10 deletions packages/frontend/src/pages/settings/accounts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<div class="">
<FormSuspense :p="init">
<div class="_gaps">
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
<div class="_buttons">
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
</div>

<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
<div class="avatar">
Expand All @@ -27,14 +30,16 @@ import { defineAsyncComponent, ref } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i, refreshAccounts } from '@/account';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import type * as Misskey from 'misskey-js';

const storedAccounts = ref<any>(null);
const accounts = ref<any>(null);
const accounts = ref<Misskey.entities.UserDetailed[]>([]);

const init = async () => {
await refreshAccounts();
getAccounts().then(accounts => {
storedAccounts.value = accounts.filter(x => x.id !== $i!.id);

Expand All @@ -52,7 +57,7 @@ function menu(account, ev) {
icon: 'ti ti-switch-horizontal',
action: () => switchAccount(account),
}, {
text: i18n.ts.remove,
text: i18n.ts.logout,
icon: 'ti ti-trash',
danger: true,
action: () => removeAccount(account),
Expand All @@ -69,23 +74,25 @@ function addAccount(ev) {
}], ev.currentTarget ?? ev.target);
}

function removeAccount(account) {
_removeAccount(account.id);
async function removeAccount(account) {
await _removeAccount(account.id);
accounts.value = accounts.value.filter(x => x.id !== account.id);
}

function addExistingAccount() {
os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
done: async res => {
await addAccounts(res.id, res.i);
os.success();
init();
},
}, 'closed');
}

function createAccount() {
os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
done: async res => {
await addAccounts(res.id, res.i);
switchAccountWithToken(res.i);
},
}, 'closed');
Expand Down