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

* refreshAccounts
Resolve #9322

* アカウント管理画面でリストを更新するように

* Update packages/frontend/src/account.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* ✌️

* クライアント起動時は現在ログインしているアカウントのみリフレッシュする

* clean up

* なんかめっちゃ変えた

* refactor

* refactor

* fix lint

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
tamaina 2023-03-09 14:27:16 +09:00 committed by GitHub
parent a4ca127ebd
commit c75afad64a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 94 deletions

View file

@ -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';
@ -7,6 +7,7 @@ import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { MenuButton } from './types/menu';
// TODO: 他のタブと永続化されたstateを同期
@ -26,11 +27,11 @@ export function incNotesCount() {
}
export async function signout() {
if (!$i) return;
waiting();
miLocalStorage.removeItem('account');
await removeAccount($i.id);
const accounts = await getAccounts();
//#region Remove service worker registration
@ -76,15 +77,19 @@ export async function addAccount(id: Account['id'], token: Account['token']) {
}
}
export async function removeAccount(id: Account['id']) {
export async function removeAccount(idOrToken: Account['id']) {
const accounts = await getAccounts();
accounts.splice(accounts.findIndex(x => x.id === id), 1);
const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
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);
} 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`, {
@ -96,44 +101,94 @@ 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 === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
// USER_IS_DELETED
// アカウントが削除されている
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.accountDeleted,
text: i18n.ts.accountDeletedDescription,
});
}
} 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.tokenRevoked,
text: i18n.ts.tokenRevokedDescription,
});
}
} 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);
}
})
.catch(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 refreshAccount() {
if (!$i) return;
return fetchAccount($i.token, $i.id)
.then(updateAccount, reason => {
if (reason === true) return signout();
return;
});
}
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 => {
if (reason === true) {
// 削除対象の場合
removeAccount(token);
}
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);
@ -155,6 +210,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 => {
@ -175,8 +232,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) {
@ -188,7 +246,7 @@ export async function openAccountMenu(opts: {
function createItem(account: misskey.entities.UserDetailed) {
return {
type: 'user',
type: 'user' as const,
user: account,
active: opts.active != null ? opts.active === account.id : false,
action: () => {
@ -201,22 +259,29 @@ export async function openAccountMenu(opts: {
};
}
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
accountsPromise.then(accounts => {
const account = accounts.find(x => x.id === a.id);
if (account == null) return res(null);
if (account == null) return res({
type: 'button' as const,
text: a.id,
action: () => {
switchAccountWithToken(a.token);
},
});
res(createItem(account));
});
}));
if (opts.withExtraOperation) {
popupMenu([...[{
type: 'link',
type: 'link' as const,
text: i18n.ts.profile,
to: `/@${ $i.username }`,
avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent',
type: 'parent' as const,
icon: 'ti ti-plus',
text: i18n.ts.addAccount,
children: [{
@ -227,7 +292,7 @@ export async function openAccountMenu(opts: {
action: () => { createAccount(); },
}],
}, {
type: 'link',
type: 'link' as const,
icon: 'ti ti-users',
text: i18n.ts.manageAccounts,
to: '/settings/accounts',