fix(frontend): ログアウトするとすべてのアカウントからログアウトされる問題を修正
This commit is contained in:
parent
bc78bb9b8e
commit
d2ce24dcc6
|
|
@ -34,6 +34,7 @@ noAccountDescription: "自己紹介はありません"
|
|||
login: "ログイン"
|
||||
loggingIn: "ログイン中"
|
||||
logout: "ログアウト"
|
||||
logoutFromAll: "すべてのアカウントからログアウト"
|
||||
signup: "新規登録"
|
||||
uploading: "アップロード中"
|
||||
save: "保存"
|
||||
|
|
@ -991,6 +992,8 @@ numberOfPageCache: "ページキャッシュ数"
|
|||
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
||||
logoutConfirm: "ログアウトしますか?"
|
||||
logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。"
|
||||
removeAccountConfirm: "{username}からログアウトしますか?"
|
||||
removeAccountWillClearClientData: "このアカウントを削除すると、このアカウントに関するクライアントの設定情報がブラウザから消去されます。再度このアカウントでログインする場合、設定情報を復元できるようにするためには、このアカウントに切り替えて、設定の自動バックアップを有効にしてください。"
|
||||
lastActiveDate: "最終利用日時"
|
||||
statusbar: "ステータスバー"
|
||||
pleaseSelect: "選択してください"
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ import { signout } from '@/signout.js';
|
|||
|
||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
export async function getAccounts(): Promise<{
|
||||
export type AccountData = {
|
||||
host: string;
|
||||
id: Misskey.entities.User['id'];
|
||||
username: Misskey.entities.User['username'];
|
||||
user?: Misskey.entities.MeDetailed | null;
|
||||
token: string | null;
|
||||
}[]> {
|
||||
};
|
||||
|
||||
export async function getAccounts(): Promise<AccountData[]> {
|
||||
const tokens = store.s.accountTokens;
|
||||
const accountInfos = store.s.accountInfos;
|
||||
const accounts = prefer.s.accounts;
|
||||
|
|
@ -162,14 +164,17 @@ export async function refreshCurrentAccount() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function login(token: AccountWithToken['token'], redirect?: string) {
|
||||
export async function login(token: AccountWithToken['token'], redirect?: string, showWaiting = true) {
|
||||
const showing = ref(true);
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||
success: false,
|
||||
showing: showing,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
if (showWaiting) {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||
success: false,
|
||||
showing: showing,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
const me = await fetchAccount(token, undefined, true).catch(reason => {
|
||||
showing.value = false;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-adaptive-bg :class="[$style.root]">
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator/>
|
||||
<div :class="$style.body">
|
||||
<span :class="$style.name"><MkUserName :user="user"/></span>
|
||||
<span :class="$style.name"><MkUserName :user="user"/><slot name="nameSuffix"></slot></span>
|
||||
<span :class="$style.sub"><span class="_monospace">@{{ acct(user) }}</span></span>
|
||||
</div>
|
||||
<MkMiniChart v-if="chartValues" :class="$style.chart" :src="chartValues"/>
|
||||
|
|
|
|||
|
|
@ -222,6 +222,45 @@ export class Pizzax<T extends StateDef> {
|
|||
return this.def[key].default;
|
||||
}
|
||||
|
||||
/** 現在のアカウントに紐づくデータをデバイスから削除します */
|
||||
public async clearCurrentAccountDataFromDevice() {
|
||||
if ($i == null) return;
|
||||
|
||||
// deviceAccount
|
||||
{
|
||||
const deviceAccountState = await get(this.deviceAccountStateKeyName) || {};
|
||||
let changed = false;
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
delete deviceAccountState[k];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await this.addIdbSetJob(async () => {
|
||||
await set(this.deviceAccountStateKeyName, deviceAccountState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// account (cacheを消す)
|
||||
{
|
||||
const registryCache = await get(this.registryCacheKeyName) || {};
|
||||
let changed = false;
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'account' && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
delete registryCache[k];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await this.addIdbSetJob(async () => {
|
||||
await set(this.registryCacheKeyName, registryCache);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue上で設定コントロールのmodelとして使う用
|
||||
|
|
|
|||
|
|
@ -8,11 +8,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
|
||||
<MkButton danger @click="logoutFromAll"><i class="ti ti-power"></i> {{ i18n.ts.logoutFromAll }}</MkButton>
|
||||
<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
|
||||
</div>
|
||||
|
||||
<template v-for="x in accounts" :key="x.host + x.id">
|
||||
<MkUserCardMini v-if="x.user" :user="x.user" :class="$style.user" @click.prevent="showMenu(x.host, x.id, $event)"/>
|
||||
<MkUserCardMini v-if="x.user" :user="x.user" :class="$style.user" @click.prevent="showMenu(x, $event)">
|
||||
<template #nameSuffix>
|
||||
<span v-if="x.id === $i?.id" :class="$style.currentAccountTag">{{ i18n.ts.loggingIn }}</span>
|
||||
</template>
|
||||
</MkUserCardMini>
|
||||
<button v-else v-panel class="_button" :class="$style.unknownUser" @click="showMenu(x, $event)">
|
||||
<div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
|
||||
<div>
|
||||
<div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}<span v-if="x.id === $i?.id" :class="$style.currentAccountTag">{{ i18n.ts.loggingIn }}</span></div>
|
||||
<div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ x.id }}</span></div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
|
|
@ -20,36 +32,69 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host as local } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog, getAccounts } from '@/accounts.js';
|
||||
import { switchAccount, removeAccount, getAccountWithSigninDialog, getAccountWithSignupDialog, getAccounts } from '@/accounts.js';
|
||||
import type { AccountData } from '@/accounts.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { signout } from '@/signout.js';
|
||||
|
||||
const accounts = await getAccounts();
|
||||
const accounts = ref<AccountData[]>([]);
|
||||
|
||||
getAccounts().then((res) => {
|
||||
accounts.value = res;
|
||||
});
|
||||
|
||||
function refreshAllAccounts() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function showMenu(host: string, id: string, ev: MouseEvent) {
|
||||
let menu: MenuItem[];
|
||||
function showMenu(a: AccountData, ev: MouseEvent) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
menu = [{
|
||||
text: i18n.ts.switch,
|
||||
icon: 'ti ti-switch-horizontal',
|
||||
action: () => switchAccount(host, id),
|
||||
}, {
|
||||
text: i18n.ts.remove,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => removeAccount(host, id),
|
||||
}];
|
||||
if ($i != null && $i.id === a.id && ($i.host ?? local) === a.host) {
|
||||
menu.push({
|
||||
text: i18n.ts.logout,
|
||||
icon: 'ti ti-power',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.logoutConfirm,
|
||||
text: i18n.ts.logoutWillClearClientData,
|
||||
});
|
||||
if (canceled) return;
|
||||
signout();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
text: i18n.ts.switch,
|
||||
icon: 'ti ti-switch-horizontal',
|
||||
action: () => switchAccount(a.host, a.id),
|
||||
}, {
|
||||
text: i18n.ts.logout,
|
||||
icon: 'ti ti-power',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.tsx.removeAccountConfirm({ username: `<plain>@${a.username}</plain>` }),
|
||||
text: i18n.ts.removeAccountWillClearClientData,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.promiseDialog((async () => {
|
||||
await removeAccount(a.host, a.id);
|
||||
accounts.value = await getAccounts();
|
||||
})());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
@ -64,20 +109,30 @@ function addAccount(ev: MouseEvent) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function addExistingAccount() {
|
||||
getAccountWithSigninDialog().then((res) => {
|
||||
if (res != null) {
|
||||
os.success();
|
||||
}
|
||||
});
|
||||
async function addExistingAccount() {
|
||||
const res = await getAccountWithSigninDialog();
|
||||
if (res != null) {
|
||||
os.success();
|
||||
}
|
||||
accounts.value = await getAccounts();
|
||||
}
|
||||
|
||||
function createAccount() {
|
||||
getAccountWithSignupDialog().then((res) => {
|
||||
if (res != null) {
|
||||
login(res.token);
|
||||
}
|
||||
async function createAccount() {
|
||||
const res = await getAccountWithSignupDialog()
|
||||
if (res != null) {
|
||||
os.success();
|
||||
}
|
||||
accounts.value = await getAccounts();
|
||||
}
|
||||
|
||||
async function logoutFromAll() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.logoutConfirm,
|
||||
text: i18n.ts.logoutWillClearClientData,
|
||||
});
|
||||
if (canceled) return;
|
||||
signout(true);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
@ -95,6 +150,16 @@ definePage(() => ({
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.currentAccountTag {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 0 6px;
|
||||
font-size: 0.8em;
|
||||
background: var(--MI_THEME-accentedBg);
|
||||
color: var(--MI_THEME-accent);
|
||||
border-radius: calc(var(--MI-radius) / 2);
|
||||
}
|
||||
|
||||
.unknownUser {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ export type StorageProvider = {
|
|||
|
||||
type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => infer R ? R : Default> = {
|
||||
default: Default;
|
||||
/** アカウントごとに異なる設定値をもたせるかどうか */
|
||||
accountDependent?: boolean;
|
||||
/** サーバーごとに異なる設定値をもたせるかどうか(他のサーバーを同一クライアントから操作できるようになった際に使用) */
|
||||
serverDependent?: boolean;
|
||||
mergeStrategy?: (a: T, b: T) => T;
|
||||
};
|
||||
|
|
@ -447,6 +449,34 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
|||
this.save();
|
||||
}
|
||||
|
||||
/** 現在の操作アカウントに紐づく設定値をデバイスから削除します(ログアウト時) */
|
||||
public clearCurrentAccountSettingsFromDevice() {
|
||||
const currentAccount = this.currentAccount; // TSを黙らせるため
|
||||
if (currentAccount == null) return;
|
||||
|
||||
let changed = false;
|
||||
|
||||
for (const _key in PREF_DEF) {
|
||||
const key = _key as keyof PREF;
|
||||
const records = this.profile.preferences[key];
|
||||
|
||||
const index = records.findIndex((record: PrefRecord<typeof key>) => {
|
||||
const scope = parseScope(record[0]);
|
||||
return scope.server === host && scope.account === currentAccount.id;
|
||||
});
|
||||
if (index === -1) continue;
|
||||
|
||||
records.splice(index, 1);
|
||||
changed = true;
|
||||
|
||||
this.rewriteRawState(key, this.getMatchedRecordOf(key)[1]);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public isSyncEnabled<K extends keyof PREF>(key: K): boolean {
|
||||
return this.getMatchedRecordOf(key)[2].sync ?? false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,18 @@
|
|||
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import { cloudBackup } from '@/preferences/utility.js';
|
||||
import { removeAccount, login } from '@/accounts.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { store } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { waiting } from '@/os.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import { clear } from '@/utility/idb-proxy.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
export async function signout() {
|
||||
if (!$i) return;
|
||||
|
||||
waiting();
|
||||
|
||||
if (store.s.enablePreferencesAutoCloudBackup) {
|
||||
await cloudBackup();
|
||||
}
|
||||
/** クライアントに保存しているすべてのデータを削除します。 */
|
||||
async function removeAllData() {
|
||||
if ($i == null) return;
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
|
|
@ -74,6 +72,54 @@ export async function signout() {
|
|||
// nothing
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
/** 現在のアカウントに関連するデータを削除します。 */
|
||||
async function removeCurrentAccountData() {
|
||||
if ($i == null) return;
|
||||
|
||||
// 設定・状態を削除
|
||||
prefer.clearCurrentAccountSettingsFromDevice();
|
||||
await store.clearCurrentAccountDataFromDevice();
|
||||
}
|
||||
|
||||
export async function signout(all = false) {
|
||||
if (!$i) return;
|
||||
|
||||
const currentAccountId = $i.id;
|
||||
|
||||
waiting();
|
||||
|
||||
if (store.s.enablePreferencesAutoCloudBackup) {
|
||||
await cloudBackup();
|
||||
}
|
||||
|
||||
if (prefer.s.accounts.length <= 1 || all) {
|
||||
// 最後のアカウントを削除する場合・全てのアカウントからログアウトする場合は全データ削除
|
||||
await removeAllData();
|
||||
} else {
|
||||
// 複数アカウントある場合は現在のアカウントのデータのみ削除
|
||||
await removeCurrentAccountData();
|
||||
|
||||
// 現在のアカウント情報を削除
|
||||
await removeAccount(host, $i.id);
|
||||
|
||||
// アカウント切り替え
|
||||
const nextAccountToken = Object.entries(store.s.accountTokens).find(([key, _]) => {
|
||||
const [accountHost, userId] = key.split('/');
|
||||
return accountHost === host && userId !== currentAccountId;
|
||||
})?.[1];
|
||||
|
||||
if (nextAccountToken != null) {
|
||||
// ログインの際の遷移の挙動はlogin関数内で行うのでここではunisonReloadを呼ばず終了
|
||||
await login(nextAccountToken, undefined, false);
|
||||
return;
|
||||
} else {
|
||||
// 現時点では外部ホストのアカウントをログインさせることはできないので、
|
||||
// 通常の全アカウントからのログアウトと同様に扱う(全データ削除)
|
||||
await removeAllData();
|
||||
}
|
||||
}
|
||||
|
||||
unisonReload('/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@ export const store = markRaw(new Pizzax('base', {
|
|||
},
|
||||
accountTokens: {
|
||||
where: 'device',
|
||||
default: {} as Record<string, string>, // host/userId, token
|
||||
default: {} as Record<`${string}/${string}`, string>, // host/userId, token
|
||||
},
|
||||
accountInfos: {
|
||||
where: 'device',
|
||||
default: {} as Record<string, Misskey.entities.MeDetailed>, // host/userId, user
|
||||
default: {} as Record<`${string}/${string}`, Misskey.entities.MeDetailed>, // host/userId, user
|
||||
},
|
||||
|
||||
enablePreferencesAutoCloudBackup: {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ export interface Locale extends ILocale {
|
|||
* ログアウト
|
||||
*/
|
||||
"logout": string;
|
||||
/**
|
||||
* すべてのアカウントからログアウト
|
||||
*/
|
||||
"logoutFromAll": string;
|
||||
/**
|
||||
* 新規登録
|
||||
*/
|
||||
|
|
@ -3976,6 +3980,14 @@ export interface Locale extends ILocale {
|
|||
* ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。
|
||||
*/
|
||||
"logoutWillClearClientData": string;
|
||||
/**
|
||||
* {username}からログアウトしますか?
|
||||
*/
|
||||
"removeAccountConfirm": ParameterizedString<"username">;
|
||||
/**
|
||||
* このアカウントを削除すると、このアカウントに関するクライアントの設定情報がブラウザから消去されます。再度このアカウントでログインする場合、設定情報を復元できるようにするためには、このアカウントに切り替えて、設定の自動バックアップを有効にしてください。
|
||||
*/
|
||||
"removeAccountWillClearClientData": string;
|
||||
/**
|
||||
* 最終利用日時
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue