mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-27 06:18:40 +09:00
enhance(frontend): 外部アプリ認証画面の改良 (misskey-dev#14828)
Cherry-picked from 076cc953e2bcd9f7335e2d9799cdf902829816cb Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
parent
62e801bf3a
commit
d4bbae8d45
@ -1269,6 +1269,33 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
|
|||||||
noDescription: "There is not the explanation"
|
noDescription: "There is not the explanation"
|
||||||
alwaysConfirmFollow: "Always confirm when following"
|
alwaysConfirmFollow: "Always confirm when following"
|
||||||
inquiry: "Contact"
|
inquiry: "Contact"
|
||||||
|
tryAgain: "Please try again later"
|
||||||
|
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
|
||||||
|
sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
|
||||||
|
createdLists: "Created lists"
|
||||||
|
createdAntennas: "Created antennas"
|
||||||
|
fromX: "From {x}"
|
||||||
|
genEmbedCode: "Generate embed code"
|
||||||
|
noteOfThisUser: "Notes by this user"
|
||||||
|
clipNoteLimitExceeded: "No more notes can be added to this clip."
|
||||||
|
performance: "Performance"
|
||||||
|
modified: "Modified"
|
||||||
|
discard: "Discard"
|
||||||
|
thereAreNChanges: "There are {n} change(s)"
|
||||||
|
signinWithPasskey: "Sign in with Passkey"
|
||||||
|
unknownWebAuthnKey: "Unknown Passkey"
|
||||||
|
passkeyVerificationFailed: "Passkey verification has failed."
|
||||||
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
|
||||||
|
messageToFollower: "Message to followers"
|
||||||
|
target: "Target"
|
||||||
|
testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\n<strong>Do not use in a production environment.</strong>"
|
||||||
|
prohibitedWordsForNameOfUser: "Prohibited words for user names"
|
||||||
|
prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction."
|
||||||
|
yourNameContainsProhibitedWords: "Your name contains prohibited words"
|
||||||
|
yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator."
|
||||||
|
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
|
||||||
|
lockdown: "Lockdown"
|
||||||
|
pleaseSelectAccount: "Select an account"
|
||||||
here: "here"
|
here: "here"
|
||||||
mutualLink: "Mutual Link"
|
mutualLink: "Mutual Link"
|
||||||
saveThisFile: "Save this file to Drive"
|
saveThisFile: "Save this file to Drive"
|
||||||
@ -2184,8 +2211,11 @@ _auth:
|
|||||||
permissionAsk: "This application requests the following permissions"
|
permissionAsk: "This application requests the following permissions"
|
||||||
pleaseGoBack: "Please go back to the application"
|
pleaseGoBack: "Please go back to the application"
|
||||||
callback: "Returning to the application"
|
callback: "Returning to the application"
|
||||||
|
accepted: "Access granted"
|
||||||
denied: "Access denied"
|
denied: "Access denied"
|
||||||
|
scopeUser: "Operate as the following user"
|
||||||
pleaseLogin: "Please log in to authorize applications."
|
pleaseLogin: "Please log in to authorize applications."
|
||||||
|
byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL"
|
||||||
_antennaSources:
|
_antennaSources:
|
||||||
all: "All notes"
|
all: "All notes"
|
||||||
homeTimeline: "Notes from followed users"
|
homeTimeline: "Notes from followed users"
|
||||||
|
120
locales/index.d.ts
vendored
120
locales/index.d.ts
vendored
@ -5114,6 +5114,114 @@ export interface Locale extends ILocale {
|
|||||||
* お問い合わせ
|
* お問い合わせ
|
||||||
*/
|
*/
|
||||||
"inquiry": string;
|
"inquiry": string;
|
||||||
|
/**
|
||||||
|
* もう一度お試しください。
|
||||||
|
*/
|
||||||
|
"tryAgain": string;
|
||||||
|
/**
|
||||||
|
* センシティブなメディアを表示するとき確認する
|
||||||
|
*/
|
||||||
|
"confirmWhenRevealingSensitiveMedia": string;
|
||||||
|
/**
|
||||||
|
* センシティブなメディアです。表示しますか?
|
||||||
|
*/
|
||||||
|
"sensitiveMediaRevealConfirm": string;
|
||||||
|
/**
|
||||||
|
* 作成したリスト
|
||||||
|
*/
|
||||||
|
"createdLists": string;
|
||||||
|
/**
|
||||||
|
* 作成したアンテナ
|
||||||
|
*/
|
||||||
|
"createdAntennas": string;
|
||||||
|
/**
|
||||||
|
* {x}から
|
||||||
|
*/
|
||||||
|
"fromX": ParameterizedString<"x">;
|
||||||
|
/**
|
||||||
|
* 埋め込みコードを生成
|
||||||
|
*/
|
||||||
|
"genEmbedCode": string;
|
||||||
|
/**
|
||||||
|
* このユーザーのノート一覧
|
||||||
|
*/
|
||||||
|
"noteOfThisUser": string;
|
||||||
|
/**
|
||||||
|
* これ以上このクリップにノートを追加できません。
|
||||||
|
*/
|
||||||
|
"clipNoteLimitExceeded": string;
|
||||||
|
/**
|
||||||
|
* パフォーマンス
|
||||||
|
*/
|
||||||
|
"performance": string;
|
||||||
|
/**
|
||||||
|
* 変更あり
|
||||||
|
*/
|
||||||
|
"modified": string;
|
||||||
|
/**
|
||||||
|
* 破棄
|
||||||
|
*/
|
||||||
|
"discard": string;
|
||||||
|
/**
|
||||||
|
* {n}件の変更があります
|
||||||
|
*/
|
||||||
|
"thereAreNChanges": ParameterizedString<"n">;
|
||||||
|
/**
|
||||||
|
* パスキーでログイン
|
||||||
|
*/
|
||||||
|
"signinWithPasskey": string;
|
||||||
|
/**
|
||||||
|
* 登録されていないパスキーです。
|
||||||
|
*/
|
||||||
|
"unknownWebAuthnKey": string;
|
||||||
|
/**
|
||||||
|
* パスキーの検証に失敗しました。
|
||||||
|
*/
|
||||||
|
"passkeyVerificationFailed": string;
|
||||||
|
/**
|
||||||
|
* パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。
|
||||||
|
*/
|
||||||
|
"passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
|
||||||
|
/**
|
||||||
|
* フォロワーへのメッセージ
|
||||||
|
*/
|
||||||
|
"messageToFollower": string;
|
||||||
|
/**
|
||||||
|
* 対象
|
||||||
|
*/
|
||||||
|
"target": string;
|
||||||
|
/**
|
||||||
|
* CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
|
||||||
|
*/
|
||||||
|
"testCaptchaWarning": string;
|
||||||
|
/**
|
||||||
|
* 禁止ワード(ユーザーの名前)
|
||||||
|
*/
|
||||||
|
"prohibitedWordsForNameOfUser": string;
|
||||||
|
/**
|
||||||
|
* このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。
|
||||||
|
*/
|
||||||
|
"prohibitedWordsForNameOfUserDescription": string;
|
||||||
|
/**
|
||||||
|
* 変更しようとした名前に禁止された文字列が含まれています
|
||||||
|
*/
|
||||||
|
"yourNameContainsProhibitedWords": string;
|
||||||
|
/**
|
||||||
|
* 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
|
||||||
|
*/
|
||||||
|
"yourNameContainsProhibitedWordsDescription": string;
|
||||||
|
/**
|
||||||
|
* 投稿者により、表示にはログインが必要と設定されています
|
||||||
|
*/
|
||||||
|
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
|
||||||
|
/**
|
||||||
|
* ロックダウン
|
||||||
|
*/
|
||||||
|
"lockdown": string;
|
||||||
|
/**
|
||||||
|
* アカウントを選択してください
|
||||||
|
*/
|
||||||
|
"pleaseSelectAccount": string;
|
||||||
/**
|
/**
|
||||||
* 通報の種類
|
* 通報の種類
|
||||||
*/
|
*/
|
||||||
@ -8525,14 +8633,26 @@ export interface Locale extends ILocale {
|
|||||||
* アプリケーションに戻っています
|
* アプリケーションに戻っています
|
||||||
*/
|
*/
|
||||||
"callback": string;
|
"callback": string;
|
||||||
|
/**
|
||||||
|
* アクセスを許可しました
|
||||||
|
*/
|
||||||
|
"accepted": string;
|
||||||
/**
|
/**
|
||||||
* アクセスを拒否しました
|
* アクセスを拒否しました
|
||||||
*/
|
*/
|
||||||
"denied": string;
|
"denied": string;
|
||||||
|
/**
|
||||||
|
* 以下のユーザーとして操作しています
|
||||||
|
*/
|
||||||
|
"scopeUser": string;
|
||||||
/**
|
/**
|
||||||
* アプリケーションにアクセス許可を与えるには、ログインが必要です。
|
* アプリケーションにアクセス許可を与えるには、ログインが必要です。
|
||||||
*/
|
*/
|
||||||
"pleaseLogin": string;
|
"pleaseLogin": string;
|
||||||
|
/**
|
||||||
|
* アクセスを許可すると、自動で以下のURLに遷移します
|
||||||
|
*/
|
||||||
|
"byClickingYouWillBeRedirectedToThisUrl": string;
|
||||||
};
|
};
|
||||||
"_antennaSources": {
|
"_antennaSources": {
|
||||||
/**
|
/**
|
||||||
|
@ -1273,6 +1273,33 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
|
|||||||
noDescription: "説明文はありません"
|
noDescription: "説明文はありません"
|
||||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||||
inquiry: "お問い合わせ"
|
inquiry: "お問い合わせ"
|
||||||
|
tryAgain: "もう一度お試しください。"
|
||||||
|
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
|
||||||
|
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||||
|
createdLists: "作成したリスト"
|
||||||
|
createdAntennas: "作成したアンテナ"
|
||||||
|
fromX: "{x}から"
|
||||||
|
genEmbedCode: "埋め込みコードを生成"
|
||||||
|
noteOfThisUser: "このユーザーのノート一覧"
|
||||||
|
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||||
|
performance: "パフォーマンス"
|
||||||
|
modified: "変更あり"
|
||||||
|
discard: "破棄"
|
||||||
|
thereAreNChanges: "{n}件の変更があります"
|
||||||
|
signinWithPasskey: "パスキーでログイン"
|
||||||
|
unknownWebAuthnKey: "登録されていないパスキーです。"
|
||||||
|
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
||||||
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
||||||
|
messageToFollower: "フォロワーへのメッセージ"
|
||||||
|
target: "対象"
|
||||||
|
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
|
||||||
|
prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
|
||||||
|
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
|
||||||
|
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
|
||||||
|
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
|
||||||
|
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
|
||||||
|
lockdown: "ロックダウン"
|
||||||
|
pleaseSelectAccount: "アカウントを選択してください"
|
||||||
abuseReportCategory: "通報の種類"
|
abuseReportCategory: "通報の種類"
|
||||||
selectCategory: "カテゴリを選択"
|
selectCategory: "カテゴリを選択"
|
||||||
reportComplete: "通報完了"
|
reportComplete: "通報完了"
|
||||||
@ -2230,8 +2257,11 @@ _auth:
|
|||||||
permissionAsk: "このアプリは次の権限を要求しています"
|
permissionAsk: "このアプリは次の権限を要求しています"
|
||||||
pleaseGoBack: "アプリケーションに戻ってやっていってください"
|
pleaseGoBack: "アプリケーションに戻ってやっていってください"
|
||||||
callback: "アプリケーションに戻っています"
|
callback: "アプリケーションに戻っています"
|
||||||
|
accepted: "アクセスを許可しました"
|
||||||
denied: "アクセスを拒否しました"
|
denied: "アクセスを拒否しました"
|
||||||
|
scopeUser: "以下のユーザーとして操作しています"
|
||||||
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
|
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
|
||||||
|
byClickingYouWillBeRedirectedToThisUrl: "アクセスを許可すると、自動で以下のURLに遷移します"
|
||||||
|
|
||||||
_antennaSources:
|
_antennaSources:
|
||||||
all: "全てのノート"
|
all: "全てのノート"
|
||||||
|
@ -1262,6 +1262,37 @@ useTotp: "일회용 비밀번호 사용"
|
|||||||
useBackupCode: "백업 코드 사용"
|
useBackupCode: "백업 코드 사용"
|
||||||
launchApp: "앱 실행"
|
launchApp: "앱 실행"
|
||||||
useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
|
useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
|
||||||
|
keepOriginalFilename: "원본 파일 이름을 유지"
|
||||||
|
keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다."
|
||||||
|
noDescription: "설명문이 없습니다"
|
||||||
|
alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
|
||||||
|
inquiry: "문의하기"
|
||||||
|
tryAgain: "다시 시도해 주세요."
|
||||||
|
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
|
||||||
|
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
|
||||||
|
createdLists: "만든 리스트"
|
||||||
|
createdAntennas: "만든 안테나"
|
||||||
|
fromX: "{x}부터"
|
||||||
|
genEmbedCode: "임베디드 코드 만들기"
|
||||||
|
noteOfThisUser: "이 유저의 노트 목록"
|
||||||
|
clipNoteLimitExceeded: "더 이상 이 클립에 노트를 추가 할 수 없습니다."
|
||||||
|
performance: "퍼포먼스"
|
||||||
|
modified: "변경 있음"
|
||||||
|
discard: "파기"
|
||||||
|
thereAreNChanges: "{n}건 변경이 있습니다."
|
||||||
|
signinWithPasskey: "패스키로 로그인"
|
||||||
|
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
|
||||||
|
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
|
||||||
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
|
||||||
|
messageToFollower: "팔로워에 보낼 메시지"
|
||||||
|
target: "대상"
|
||||||
|
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
|
||||||
|
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
|
||||||
|
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
|
||||||
|
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
|
||||||
|
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
|
||||||
|
lockdown: "잠금"
|
||||||
|
pleaseSelectAccount: "계정을 선택해 주세요"
|
||||||
here: "여기"
|
here: "여기"
|
||||||
mutualLink: "서로링크"
|
mutualLink: "서로링크"
|
||||||
saveThisFile: "이 파일을 드라이브에 저장"
|
saveThisFile: "이 파일을 드라이브에 저장"
|
||||||
@ -2166,8 +2197,11 @@ _auth:
|
|||||||
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
|
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
|
||||||
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
|
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
|
||||||
callback: "앱으로 돌아갑니다"
|
callback: "앱으로 돌아갑니다"
|
||||||
|
accepted: "접근이 허가되었습니다"
|
||||||
denied: "접근이 거부되었습니다"
|
denied: "접근이 거부되었습니다"
|
||||||
|
scopeUser: "다음 사용자로서 작업 중"
|
||||||
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
|
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
|
||||||
|
byClickingYouWillBeRedirectedToThisUrl: "접근을 허가하면 자동으로 다음 URL로 이동합니다"
|
||||||
_antennaSources:
|
_antennaSources:
|
||||||
all: "모든 노트"
|
all: "모든 노트"
|
||||||
homeTimeline: "팔로우중인 유저의 노트"
|
homeTimeline: "팔로우중인 유저의 노트"
|
||||||
|
@ -10,7 +10,7 @@ import '@/style.scss';
|
|||||||
import { mainBoot } from '@/boot/main-boot.js';
|
import { mainBoot } from '@/boot/main-boot.js';
|
||||||
import { subBoot } from '@/boot/sub-boot.js';
|
import { subBoot } from '@/boot/sub-boot.js';
|
||||||
|
|
||||||
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
|
const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/sso', '/signup-complete'];
|
||||||
|
|
||||||
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
|
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
|
||||||
subBoot();
|
subBoot();
|
||||||
|
@ -232,24 +232,6 @@ export async function openAccountMenu(opts: {
|
|||||||
}, ev: MouseEvent) {
|
}, ev: MouseEvent) {
|
||||||
if (!$i) return;
|
if (!$i) return;
|
||||||
|
|
||||||
function showSigninDialog() {
|
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
|
||||||
done: res => {
|
|
||||||
addAccount(res.id, res.i);
|
|
||||||
success();
|
|
||||||
},
|
|
||||||
}, 'closed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAccount() {
|
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
|
||||||
done: res => {
|
|
||||||
addAccount(res.id, res.i);
|
|
||||||
switchAccountWithToken(res.i);
|
|
||||||
},
|
|
||||||
}, 'closed');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||||
const storedAccounts = await getAccounts();
|
const storedAccounts = await getAccounts();
|
||||||
const found = storedAccounts.find(x => x.id === account.id);
|
const found = storedAccounts.find(x => x.id === account.id);
|
||||||
@ -306,10 +288,22 @@ export async function openAccountMenu(opts: {
|
|||||||
text: i18n.ts.addAccount,
|
text: i18n.ts.addAccount,
|
||||||
children: [{
|
children: [{
|
||||||
text: i18n.ts.existingAccount,
|
text: i18n.ts.existingAccount,
|
||||||
action: () => { showSigninDialog(); },
|
action: () => {
|
||||||
|
getAccountWithSigninDialog().then(res => {
|
||||||
|
if (res != null) {
|
||||||
|
success();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.createAccount,
|
text: i18n.ts.createAccount,
|
||||||
action: () => { createAccount(); },
|
action: () => {
|
||||||
|
getAccountWithSignupDialog().then(res => {
|
||||||
|
if (res != null) {
|
||||||
|
switchAccountWithToken(res.token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
type: 'link' as const,
|
type: 'link' as const,
|
||||||
@ -326,6 +320,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_) {
|
if (_DEV_) {
|
||||||
(window as any).$i = $i;
|
(window as any).$i = $i;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MkAuthConfirm from './MkAuthConfirm.vue';
|
||||||
|
void MkAuthConfirm;
|
450
packages/frontend/src/components/MkAuthConfirm.vue
Normal file
450
packages/frontend/src/components/MkAuthConfirm.vue
Normal file
@ -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(--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(--divider);
|
||||||
|
background-color: #fff;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconFallback {
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--accentedBg);
|
||||||
|
color: var(--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(--radius);
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionListWrapper {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: var(--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(--radius);
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
overflow: hidden;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountSelectorRadio {
|
||||||
|
position: absolute;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:focus-visible + .accountSelectorItem {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked:focus-visible + .accountSelectorItem {
|
||||||
|
outline-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .accountSelectorItem {
|
||||||
|
background: color-mix(in srgb, var(--accent), transparent 50%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountSelectorItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--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(--accentedBg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 45px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountSelectorName {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountSelectorAcct {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
@ -26,12 +26,12 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
|
|||||||
import MkModal from './MkModal.vue';
|
import MkModal from './MkModal.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
withOkButton: boolean;
|
withOkButton?: boolean;
|
||||||
withCloseButton: boolean;
|
withCloseButton?: boolean;
|
||||||
okButtonDisabled: boolean;
|
okButtonDisabled?: boolean;
|
||||||
escKeyDisabled: boolean;
|
escKeyDisabled?: boolean;
|
||||||
width: number;
|
width?: number;
|
||||||
height: number;
|
height?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
withOkButton: false,
|
withOkButton: false,
|
||||||
withCloseButton: true,
|
withCloseButton: true,
|
||||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="500"
|
:width="500"
|
||||||
:height="600"
|
:height="600"
|
||||||
@close="dialog?.close()"
|
@close="onClose"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.signup }}</template>
|
<template #header>{{ i18n.ts.signup }}</template>
|
||||||
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:leaveToClass="$style.transition_x_leaveTo"
|
:leaveToClass="$style.transition_x_leaveTo"
|
||||||
>
|
>
|
||||||
<template v-if="!isAcceptedServerRule">
|
<template v-if="!isAcceptedServerRule">
|
||||||
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
|
<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||||
@ -47,7 +47,8 @@ const props = withDefaults(defineProps<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', res: Misskey.entities.SigninResponse): void;
|
(ev: 'done', res: Misskey.entities.SignupResponse): void;
|
||||||
|
(ev: 'cancelled'): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -55,7 +56,12 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
|||||||
|
|
||||||
const isAcceptedServerRule = ref(false);
|
const isAcceptedServerRule = ref(false);
|
||||||
|
|
||||||
function onSignup(res: Misskey.entities.SigninResponse) {
|
function onClose() {
|
||||||
|
emit('cancelled');
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSignup(res: Misskey.entities.SignupResponse) {
|
||||||
emit('done', res);
|
emit('done', res);
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<div>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<MkAnimBg style="position: fixed; top: 0;"/>
|
||||||
<MkSpacer :contentMax="800">
|
<div :class="$style.formContainer">
|
||||||
<div v-if="$i">
|
<div :class="$style.form">
|
||||||
<div v-if="state == 'waiting'">
|
<MkAuthConfirm
|
||||||
<MkLoading/>
|
ref="authRoot"
|
||||||
</div>
|
:name="name"
|
||||||
<div v-if="state == 'denied'">
|
:icon="icon || undefined"
|
||||||
<p>{{ i18n.ts._auth.denied }}</p>
|
:permissions="_permissions"
|
||||||
</div>
|
@accept="onAccept"
|
||||||
<div v-else-if="state == 'accepted'" class="accepted">
|
@deny="onDeny"
|
||||||
<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
>
|
||||||
<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
|
<template #consentAdditionalInfo>
|
||||||
</div>
|
<div v-if="callback != null" :class="$style.redirectRoot">
|
||||||
<div v-else>
|
<div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div>
|
||||||
<div v-if="_permissions.length > 0">
|
<div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div>
|
||||||
<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
|
</div>
|
||||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
</template>
|
||||||
<ul>
|
</MkAuthConfirm>
|
||||||
<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>
|
</div>
|
||||||
<div v-else>
|
</div>
|
||||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
</div>
|
||||||
<MkSignin @login="onLogin"/>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { computed, useTemplateRef } from 'vue';
|
||||||
import MkSignin from '@/components/MkSignin.vue';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import { $i, login } from '@/account.js';
|
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
|
||||||
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
session: string;
|
session: string;
|
||||||
callback?: string;
|
callback?: string;
|
||||||
name: string;
|
name?: string;
|
||||||
icon: string;
|
icon?: string;
|
||||||
permission: 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> {
|
async function onAccept(token: string) {
|
||||||
state.value = 'waiting';
|
|
||||||
await misskeyApi('miauth/gen-token', {
|
await misskeyApi('miauth/gen-token', {
|
||||||
session: props.session,
|
session: props.session,
|
||||||
name: props.name,
|
name: props.name,
|
||||||
iconUrl: props.icon,
|
iconUrl: props.icon,
|
||||||
permission: _permissions,
|
permission: _permissions.value,
|
||||||
|
}, token).catch(() => {
|
||||||
|
authRoot.value?.showUI('failed');
|
||||||
});
|
});
|
||||||
|
|
||||||
state.value = 'accepted';
|
if (props.callback && props.callback !== '') {
|
||||||
if (props.callback) {
|
|
||||||
const cbUrl = new URL(props.callback);
|
const cbUrl = new URL(props.callback);
|
||||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||||
cbUrl.searchParams.set('session', props.session);
|
cbUrl.searchParams.set('session', props.session);
|
||||||
location.href = cbUrl.href;
|
location.href = cbUrl.toString();
|
||||||
|
} else {
|
||||||
|
authRoot.value?.showUI('success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deny(): void {
|
function onDeny() {
|
||||||
state.value = 'denied';
|
authRoot.value?.showUI('denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLogin(res): void {
|
|
||||||
login(res.i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
title: 'MiAuth',
|
title: 'MiAuth',
|
||||||
icon: 'ti ti-apps',
|
icon: 'ti ti-apps',
|
||||||
@ -100,15 +84,38 @@ definePageMetadata(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.buttons {
|
.formContainer {
|
||||||
margin-top: 16px;
|
min-height: 100svh;
|
||||||
display: flex;
|
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
|
||||||
gap: 8px;
|
box-sizing: border-box;
|
||||||
flex-wrap: wrap;
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loginMessage {
|
.form {
|
||||||
text-align: center;
|
position: relative;
|
||||||
margin: 8px 0 24px;
|
z-index: 10;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: var(--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(--radius);
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redirectUrl {
|
||||||
|
font-size: 90%;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: var(--panel);
|
||||||
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<div>
|
||||||
<template #header><MkPageHeader/></template>
|
<MkAnimBg style="position: fixed; top: 0;"/>
|
||||||
<MkSpacer :contentMax="800">
|
<div :class="$style.formContainer">
|
||||||
<div v-if="$i">
|
<div :class="$style.form">
|
||||||
<div v-if="permissions.length > 0">
|
<MkAuthConfirm
|
||||||
<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
|
ref="authRoot"
|
||||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
:name="name"
|
||||||
<ul>
|
:permissions="permissions"
|
||||||
<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
|
:waitOnDeny="true"
|
||||||
</ul>
|
@accept="onAccept"
|
||||||
</div>
|
@deny="onDeny"
|
||||||
<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" class="mk-input-token-hidden" type="hidden" :value="$i.token"/>
|
|
||||||
<input name="transaction_id" class="mk-input-tr-id-hidden" type="hidden" :value="transactionIdMeta?.content"/>
|
|
||||||
<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
|
|
||||||
<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
</div>
|
||||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
</div>
|
||||||
<MkSignin @login="onLogin"/>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkSignin from '@/components/MkSignin.vue';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import { $i, login } from '@/account.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
|
||||||
|
|
||||||
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
|
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
|
||||||
if (transactionIdMeta) {
|
if (transactionIdMeta) {
|
||||||
@ -45,10 +33,44 @@ if (transactionIdMeta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
|
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 onLogin(res): void {
|
function doPost(token: string, decision: 'accept' | 'deny') {
|
||||||
login(res.i);
|
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 onDeny(token: string) {
|
||||||
|
doPost(token, 'deny');
|
||||||
}
|
}
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
@ -58,15 +80,24 @@ definePageMetadata(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.buttons {
|
.formContainer {
|
||||||
margin-top: 16px;
|
min-height: 100svh;
|
||||||
display: flex;
|
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
|
||||||
gap: 8px;
|
box-sizing: border-box;
|
||||||
flex-wrap: wrap;
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loginMessage {
|
.form {
|
||||||
text-align: center;
|
position: relative;
|
||||||
margin: 8px 0 24px;
|
z-index: 10;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: var(--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>
|
</style>
|
||||||
|
@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.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 { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
@ -74,22 +74,20 @@ async function removeAccount(account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addExistingAccount() {
|
function addExistingAccount() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
getAccountWithSigninDialog().then((res) => {
|
||||||
done: async res => {
|
if (res != null) {
|
||||||
await addAccounts(res.id, res.i);
|
|
||||||
os.success();
|
os.success();
|
||||||
init();
|
init();
|
||||||
},
|
}
|
||||||
}, 'closed');
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAccount() {
|
function createAccount() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
getAccountWithSignupDialog().then((res) => {
|
||||||
done: async res => {
|
if (res != null) {
|
||||||
await addAccounts(res.id, res.i);
|
switchAccountWithToken(res.token);
|
||||||
switchAccountWithToken(res.i);
|
}
|
||||||
},
|
});
|
||||||
}, 'closed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchAccount(account: any) {
|
async function switchAccount(account: any) {
|
||||||
|
@ -1,100 +1,88 @@
|
|||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<div>
|
||||||
<template #header><MkPageHeader/></template>
|
<MkAnimBg style="position: fixed; top: 0;"/>
|
||||||
<MkSpacer :contentMax="800">
|
<div :class="$style.formContainer">
|
||||||
<div v-if="$i && !loading">
|
<div :class="$style.form">
|
||||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
<MkAuthConfirm
|
||||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
ref="authRoot"
|
||||||
<div :class="$style.buttons">
|
:name="name"
|
||||||
<MkButton @click="onCancel">{{ i18n.ts.cancel }}</MkButton>
|
@accept="onAccept"
|
||||||
<MkButton primary @click="onAccept">{{ i18n.ts.accept }}</MkButton>
|
@deny="onDeny"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="$i && loading">
|
</div>
|
||||||
<div>{{ i18n.ts._auth.callback }}</div>
|
|
||||||
<MkLoading class="loading"/>
|
|
||||||
<div style="display: none">
|
|
||||||
<form ref="postBindingForm" method="post" :action="actionUrl" autocomplete="off">
|
|
||||||
<input v-for="(value, key) in actionContext" :key="key" :name="key" :value="value" type="hidden"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
|
||||||
<MkSignin @login="onLogin"/>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, nextTick, onMounted } from 'vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
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 os from '@/os.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
|
||||||
|
import { nextTick, onMounted, useTemplateRef } from "vue";
|
||||||
|
import { $i } from "@/account.js";
|
||||||
|
|
||||||
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:transaction-id"]');
|
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:transaction-id"]');
|
||||||
if (transactionIdMeta) {
|
if (transactionIdMeta) {
|
||||||
transactionIdMeta.remove();
|
transactionIdMeta.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
|
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
|
||||||
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
|
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
|
||||||
const prompt = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:prompt"]')?.content;
|
const prompt = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:prompt"]')?.content;
|
||||||
|
|
||||||
const loading = ref(false);
|
const authRoot = useTemplateRef('authRoot');
|
||||||
const postBindingForm = ref<HTMLFormElement | null>(null);
|
|
||||||
const actionUrl = ref<string | undefined>(undefined);
|
|
||||||
const actionContext = ref<Record<string, string> | null>(null);
|
|
||||||
|
|
||||||
function onLogin(res): void {
|
async function onAccept(token: string) {
|
||||||
login(res.i);
|
const result = await fetch(`/sso/${kind}/authorize`, {
|
||||||
}
|
|
||||||
|
|
||||||
function onCancel(): void {
|
|
||||||
if (history.length > 1) history.back();
|
|
||||||
else location.href = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAccept(): void {
|
|
||||||
loading.value = true;
|
|
||||||
os.promiseDialog(authorize());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authorize(): Promise<void> {
|
|
||||||
const res = await fetch(`/sso/${kind}/authorize`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
transaction_id: transactionIdMeta?.content,
|
transaction_id: transactionIdMeta?.content,
|
||||||
login_token: $i!.token,
|
login_token: token,
|
||||||
}),
|
}),
|
||||||
|
}).then(res => res.json()).catch(() => {
|
||||||
|
authRoot.value?.showUI('failed');
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
|
||||||
if (json.binding === 'post') {
|
if (!result) return;
|
||||||
actionUrl.value = json.action;
|
authRoot.value?.showUI('success');
|
||||||
actionContext.value = json.context;
|
|
||||||
|
if (result.binding === 'post') {
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.style.display = 'none';
|
||||||
|
form.action = result.action;
|
||||||
|
form.method = 'post';
|
||||||
|
form.acceptCharset = 'utf-8';
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(result.context)) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = name;
|
||||||
|
input.value = value as string;
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
postBindingForm.value?.submit();
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
location.href = json.action;
|
location.href = result.action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDeny(token: string) {
|
||||||
|
authRoot.value?.showUI('denied');
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if ($i && prompt === 'none') {
|
nextTick(() => {
|
||||||
onAccept();
|
if ($i && prompt === 'none') {
|
||||||
}
|
onAccept($i.token);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
@ -104,15 +92,24 @@ definePageMetadata(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.buttons {
|
.formContainer {
|
||||||
margin-top: 16px;
|
min-height: 100svh;
|
||||||
display: flex;
|
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
|
||||||
gap: 8px;
|
box-sizing: border-box;
|
||||||
flex-wrap: wrap;
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loginMessage {
|
.form {
|
||||||
text-align: center;
|
position: relative;
|
||||||
margin: 8px 0 24px;
|
z-index: 10;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: var(--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>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user