mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-08 23:33:09 +02:00
enhance: 二要素認証設定時のセキュリティを強化 (#11863)
* enhance: 二要素認証設定時のセキュリティを強化 パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要にする * Update CoreModule.ts * Update 2fa.ts * wip * wip * Update 2fa.ts * tweak
This commit is contained in:
parent
eca8c7a52f
commit
c836157edb
23 changed files with 400 additions and 122 deletions
|
@ -30,6 +30,8 @@
|
|||
- Feat: プロフィールでのリンク検証
|
||||
- Feat: 通知をテストできるようになりました
|
||||
- Feat: PWAのアイコンが設定できるようになりました
|
||||
- Enhance: 二要素認証設定時のセキュリティを強化
|
||||
- パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要になりました
|
||||
- Enhance: manifest.jsonをオーバーライド可能に
|
||||
- Enhance: 依存関係の更新
|
||||
- Enhance: ローカリゼーションの更新
|
||||
|
@ -40,9 +42,7 @@
|
|||
- Feat: Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`)
|
||||
- Feat: クライアントを起動している間、デバイスの画面が自動でオフになるのを防ぐオプションを追加
|
||||
- Feat: 新しい実績を追加
|
||||
- Enhance: ノート詳細ページを改修
|
||||
- 読み込み時のパフォーマンスが向上しました
|
||||
- リノート一覧、リアクション一覧がタブとして追加されました
|
||||
- Enhance: ノート詳細ページでリノート一覧、リアクション一覧タブを追加
|
||||
- ノートのメニューからは当該項目は消えました
|
||||
- Enhance: プロフィールにその人が作ったPlayの一覧出せるように
|
||||
- Enhance: メニューのスイッチの動作を改善
|
||||
|
@ -62,6 +62,7 @@
|
|||
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
|
||||
- Enhance: Mk:apiが失敗した時にエラー型の値(AiScript 0.16.0で追加)を返すように
|
||||
- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように(試験的なためPlayなどには未実装)
|
||||
- Enhance: ノート詳細ページ読み込み時のパフォーマンスが向上しました
|
||||
- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
|
||||
- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように
|
||||
- Enhance: 細かなデザインの調整
|
||||
|
|
3
locales/index.d.ts
vendored
3
locales/index.d.ts
vendored
|
@ -1119,6 +1119,8 @@ export interface Locale {
|
|||
"verifiedLink": string;
|
||||
"notifyNotes": string;
|
||||
"unnotifyNotes": string;
|
||||
"authentication": string;
|
||||
"authenticationRequiredToContinue": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1833,7 +1835,6 @@ export interface Locale {
|
|||
"_2fa": {
|
||||
"alreadyRegistered": string;
|
||||
"registerTOTP": string;
|
||||
"passwordToTOTP": string;
|
||||
"step1": string;
|
||||
"step2": string;
|
||||
"step2Click": string;
|
||||
|
|
|
@ -1116,6 +1116,8 @@ keepScreenOn: "デバイスの画面を常にオンにする"
|
|||
verifiedLink: "このリンク先の所有者であることが確認されました"
|
||||
notifyNotes: "投稿を通知"
|
||||
unnotifyNotes: "投稿の通知を解除"
|
||||
authentication: "認証"
|
||||
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -1750,7 +1752,6 @@ _timelineTutorial:
|
|||
_2fa:
|
||||
alreadyRegistered: "既に設定は完了しています。"
|
||||
registerTOTP: "認証アプリの設定を開始"
|
||||
passwordToTOTP: "パスワードを入力してください"
|
||||
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
|
||||
step2: "次に、表示されているQRコードをアプリでスキャンします。"
|
||||
step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。"
|
||||
|
|
|
@ -51,6 +51,7 @@ import { UserKeypairService } from './UserKeypairService.js';
|
|||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
import { WebhookService } from './WebhookService.js';
|
||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||
|
@ -177,6 +178,7 @@ const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisti
|
|||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
|
@ -306,6 +308,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UtilityService,
|
||||
|
@ -428,6 +431,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UtilityService,
|
||||
|
@ -551,6 +555,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UtilityService,
|
||||
|
@ -672,6 +677,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UtilityService,
|
||||
|
|
45
packages/backend/src/core/UserAuthService.ts
Normal file
45
packages/backend/src/core/UserAuthService.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserAuthService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async twoFactorAuthenticate(profile: MiUserProfile, token: string): Promise<void> {
|
||||
if (profile.twoFactorBackupSecret?.includes(token)) {
|
||||
await this.userProfilesRepository.update({ userId: profile.userId }, {
|
||||
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
|
||||
});
|
||||
} else {
|
||||
const delta = OTPAuth.TOTP.validate({
|
||||
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
|
||||
digits: 6,
|
||||
token,
|
||||
window: 5,
|
||||
});
|
||||
|
||||
if (delta === null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import type { MiLocalUser } from '@/models/User.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||
|
@ -42,6 +43,7 @@ export class SigninApiService {
|
|||
private idService: IdService,
|
||||
private rateLimiterService: RateLimiterService,
|
||||
private signinService: SigninService,
|
||||
private userAuthService: UserAuthService,
|
||||
private webAuthnService: WebAuthnService,
|
||||
) {
|
||||
}
|
||||
|
@ -154,27 +156,15 @@ export class SigninApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (profile.twoFactorBackupSecret?.includes(token)) {
|
||||
await this.userProfilesRepository.update({ userId: profile.userId }, {
|
||||
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
|
||||
});
|
||||
return this.signinService.signin(request, reply, user);
|
||||
}
|
||||
|
||||
const delta = OTPAuth.TOTP.validate({
|
||||
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
|
||||
digits: 6,
|
||||
token,
|
||||
window: 1,
|
||||
});
|
||||
|
||||
if (delta === null) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
return await fail(403, {
|
||||
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
|
||||
});
|
||||
} else {
|
||||
return this.signinService.signin(request, reply, user);
|
||||
}
|
||||
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else if (body.credential) {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
return await fail(403, {
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
|
||||
digits: 6,
|
||||
token,
|
||||
window: 1,
|
||||
window: 5,
|
||||
});
|
||||
|
||||
if (delta === null) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -37,6 +38,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
name: { type: 'string', minLength: 1, maxLength: 30 },
|
||||
credential: { type: 'object' },
|
||||
},
|
||||
|
@ -54,16 +56,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
|
||||
private webAuthnService: WebAuthnService,
|
||||
private userAuthService: UserAuthService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -41,6 +42,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
@ -53,8 +55,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private webAuthnService: WebAuthnService,
|
||||
private userAuthService: UserAuthService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
where: {
|
||||
userId: me.id,
|
||||
|
@ -66,10 +70,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
throw new ApiError(meta.errors.userNotFound);
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -31,6 +32,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
@ -43,14 +45,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userAuthService: UserAuthService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -30,6 +31,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
credentialId: { type: 'string' },
|
||||
},
|
||||
required: ['password', 'credentialId'],
|
||||
|
@ -45,15 +47,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userAuthService: UserAuthService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -30,6 +31,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
@ -41,15 +43,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userAuthService: UserAuthService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -20,6 +21,7 @@ export const paramDef = {
|
|||
properties: {
|
||||
currentPassword: { type: 'string' },
|
||||
newPassword: { type: 'string', minLength: 1 },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['currentPassword', 'newPassword'],
|
||||
} as const;
|
||||
|
@ -29,14 +31,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userAuthService: UserAuthService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.currentPassword, profile.password!);
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.currentPassword, profile.password!);
|
||||
|
||||
if (!passwordMatched) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -20,6 +21,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
@ -33,19 +35,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userAuthService: UserAuthService,
|
||||
private deleteAccountService: DeleteAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
if (userDetailed.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
|
||||
if (!passwordMatched) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { Config } from '@/config.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -46,6 +47,7 @@ export const paramDef = {
|
|||
properties: {
|
||||
password: { type: 'string' },
|
||||
email: { type: 'string', nullable: true },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
@ -61,15 +63,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private userEntityService: UserEntityService,
|
||||
private emailService: EmailService,
|
||||
private userAuthService: UserAuthService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
|
||||
if (!passwordMatched) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
|
|
|
@ -60,10 +60,12 @@ describe('2要素認証', () => {
|
|||
};
|
||||
|
||||
const keyDoneParam = (param: {
|
||||
token: string,
|
||||
keyName: string,
|
||||
credentialId: Buffer,
|
||||
creationOptions: PublicKeyCredentialCreationOptionsJSON,
|
||||
}): {
|
||||
token: string,
|
||||
password: string,
|
||||
name: string,
|
||||
credential: RegistrationResponseJSON,
|
||||
|
@ -94,6 +96,7 @@ describe('2要素認証', () => {
|
|||
|
||||
return {
|
||||
password,
|
||||
token: param.token,
|
||||
name: param.keyName,
|
||||
credential: <RegistrationResponseJSON>{
|
||||
id: param.credentialId.toString('base64url'),
|
||||
|
@ -218,6 +221,12 @@ describe('2要素認証', () => {
|
|||
});
|
||||
assert.strictEqual(signinResponse.status, 200);
|
||||
assert.notEqual(signinResponse.body.i, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
|
||||
test('が設定でき、セキュリティキーでログインできる。', async () => {
|
||||
|
@ -233,6 +242,7 @@ describe('2要素認証', () => {
|
|||
|
||||
const registerKeyResponse = await api('/i/2fa/register-key', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
assert.strictEqual(registerKeyResponse.status, 200);
|
||||
assert.notEqual(registerKeyResponse.body.rp, undefined);
|
||||
|
@ -241,6 +251,7 @@ describe('2要素認証', () => {
|
|||
const keyName = 'example-key';
|
||||
const credentialId = crypto.randomBytes(0x41);
|
||||
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
|
@ -271,6 +282,12 @@ describe('2要素認証', () => {
|
|||
}));
|
||||
assert.strictEqual(signinResponse2.status, 200);
|
||||
assert.notEqual(signinResponse2.body.i, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
|
||||
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
|
||||
|
@ -285,6 +302,7 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(doneResponse.status, 200);
|
||||
|
||||
const registerKeyResponse = await api('/i/2fa/register-key', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
password,
|
||||
}, alice);
|
||||
assert.strictEqual(registerKeyResponse.status, 200);
|
||||
|
@ -292,6 +310,7 @@ describe('2要素認証', () => {
|
|||
const keyName = 'example-key';
|
||||
const credentialId = crypto.randomBytes(0x41);
|
||||
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
|
@ -326,6 +345,12 @@ describe('2要素認証', () => {
|
|||
});
|
||||
assert.strictEqual(signinResponse2.status, 200);
|
||||
assert.notEqual(signinResponse2.body.i, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
|
||||
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
|
||||
|
@ -340,6 +365,7 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(doneResponse.status, 200);
|
||||
|
||||
const registerKeyResponse = await api('/i/2fa/register-key', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
password,
|
||||
}, alice);
|
||||
assert.strictEqual(registerKeyResponse.status, 200);
|
||||
|
@ -347,6 +373,7 @@ describe('2要素認証', () => {
|
|||
const keyName = 'example-key';
|
||||
const credentialId = crypto.randomBytes(0x41);
|
||||
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
|
@ -367,6 +394,12 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(securityKeys.length, 1);
|
||||
assert.strictEqual(securityKeys[0].name, renamedKey);
|
||||
assert.notEqual(securityKeys[0].lastUsed, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
|
||||
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
|
||||
|
@ -381,6 +414,7 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(doneResponse.status, 200);
|
||||
|
||||
const registerKeyResponse = await api('/i/2fa/register-key', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
password,
|
||||
}, alice);
|
||||
assert.strictEqual(registerKeyResponse.status, 200);
|
||||
|
@ -388,6 +422,7 @@ describe('2要素認証', () => {
|
|||
const keyName = 'example-key';
|
||||
const credentialId = crypto.randomBytes(0x41);
|
||||
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
|
@ -400,6 +435,7 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(iResponse.status, 200);
|
||||
for (const key of iResponse.body.securityKeysList) {
|
||||
const removeKeyResponse = await api('/i/2fa/remove-key', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
password,
|
||||
credentialId: key.id,
|
||||
}, alice);
|
||||
|
@ -418,6 +454,12 @@ describe('2要素認証', () => {
|
|||
});
|
||||
assert.strictEqual(signinResponse.status, 200);
|
||||
assert.notEqual(signinResponse.body.i, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
|
||||
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
|
||||
|
@ -438,6 +480,7 @@ describe('2要素認証', () => {
|
|||
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
|
||||
|
||||
const unregisterResponse = await api('/i/2fa/unregister', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
password,
|
||||
}, alice);
|
||||
assert.strictEqual(unregisterResponse.status, 204);
|
||||
|
@ -447,5 +490,11 @@ describe('2要素認証', () => {
|
|||
});
|
||||
assert.strictEqual(signinResponse.status, 200);
|
||||
assert.notEqual(signinResponse.body.i, undefined);
|
||||
|
||||
// 後片付け
|
||||
await api('/i/2fa/unregister', {
|
||||
password,
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
}, alice);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -155,6 +155,10 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
70
packages/frontend/src/components/MkPasswordDialog.vue
Normal file
70
packages/frontend/src/components/MkPasswordDialog.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
@close="onClose"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.authentication }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div style="padding: 0 0 16px 0; text-align: center;">
|
||||
<i class="ti ti-lock" style="font-size: 32px; color: var(--accent);"></i>
|
||||
<div style="margin-top: 10px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
|
||||
</div>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true">
|
||||
<template #prefix><i class="ti ti-password"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false">
|
||||
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
|
||||
<template #prefix><i class="ti ti-123"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { password: string; token: string | null; }): void;
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'cancelled'): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const passwordInput = $shallowRef<InstanceType<typeof MkInput>>();
|
||||
const password = $ref('');
|
||||
const token = $ref(null);
|
||||
|
||||
function onClose() {
|
||||
emit('cancelled');
|
||||
if (dialog) dialog.close();
|
||||
}
|
||||
|
||||
function done(res) {
|
||||
emit('done', { password, token });
|
||||
if (dialog) dialog.close();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (passwordInput) passwordInput.focus();
|
||||
});
|
||||
</script>
|
|
@ -17,6 +17,7 @@ import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
|
|||
import MkPageWindow from '@/components/MkPageWindow.vue';
|
||||
import MkToast from '@/components/MkToast.vue';
|
||||
import MkDialog from '@/components/MkDialog.vue';
|
||||
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
|
||||
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
||||
import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
|
||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||
|
@ -333,6 +334,18 @@ export function inputDate(props: {
|
|||
});
|
||||
}
|
||||
|
||||
export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | {
|
||||
canceled: false; result: { password: string; token: string | null; };
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(MkPasswordDialog, {}, {
|
||||
done: result => {
|
||||
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
export function select<C = any>(props: {
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
|
|
|
@ -94,16 +94,12 @@ withDefaults(defineProps<{
|
|||
const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false);
|
||||
|
||||
async function registerTOTP(): Promise<void> {
|
||||
const password = await os.inputText({
|
||||
title: i18n.ts._2fa.registerTOTP,
|
||||
text: i18n.ts._2fa.passwordToTOTP,
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
});
|
||||
if (password.canceled) return;
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
const twoFactorData = await os.apiWithDialog('i/2fa/register', {
|
||||
password: password.result,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
});
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), {
|
||||
|
@ -111,22 +107,19 @@ async function registerTOTP(): Promise<void> {
|
|||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function unregisterTOTP(): void {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
async function unregisterTOTP(): Promise<void> {
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
os.apiWithDialog('i/2fa/unregister', {
|
||||
password: password,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
}).catch(error => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: error,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renewTOTP(): void {
|
||||
|
@ -150,15 +143,12 @@ async function unregisterKey(key) {
|
|||
});
|
||||
if (confirm.canceled) return;
|
||||
|
||||
const password = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
});
|
||||
if (password.canceled) return;
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/2fa/remove-key', {
|
||||
password: password.result,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
credentialId: key.id,
|
||||
});
|
||||
os.success();
|
||||
|
@ -181,16 +171,13 @@ async function renameKey(key) {
|
|||
}
|
||||
|
||||
async function addSecurityKey() {
|
||||
const password = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
});
|
||||
if (password.canceled) return;
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
const registrationOptions = parseCreationOptionsFromJSON({
|
||||
publicKey: await os.apiWithDialog('i/2fa/register-key', {
|
||||
password: password.result,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -211,8 +198,12 @@ async function addSecurityKey() {
|
|||
);
|
||||
if (!credential) return;
|
||||
|
||||
const auth2 = await os.authenticateDialog();
|
||||
if (auth2.canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/2fa/key-done', {
|
||||
password: password.result,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
name: name.result,
|
||||
credential: credential.toJSON(),
|
||||
});
|
||||
|
|
|
@ -67,18 +67,16 @@ const onChangeReceiveAnnouncementEmail = (v) => {
|
|||
});
|
||||
};
|
||||
|
||||
const saveEmailAddress = () => {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
async function saveEmailAddress() {
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
os.apiWithDialog('i/update-email', {
|
||||
password: password,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
email: emailAddress.value,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention'));
|
||||
const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply'));
|
||||
|
|
|
@ -113,14 +113,12 @@ async function deleteAccount() {
|
|||
if (canceled) return;
|
||||
}
|
||||
|
||||
const { canceled, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled) return;
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password,
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
});
|
||||
|
||||
await os.alert({
|
||||
|
|
|
@ -55,13 +55,6 @@ const pagination = {
|
|||
};
|
||||
|
||||
async function change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText({
|
||||
title: i18n.ts.currentPassword,
|
||||
type: 'password',
|
||||
autocomplete: 'current-password',
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
title: i18n.ts.newPassword,
|
||||
type: 'password',
|
||||
|
@ -84,21 +77,23 @@ async function change() {
|
|||
return;
|
||||
}
|
||||
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword,
|
||||
currentPassword: auth.result.password,
|
||||
token: auth.result.token,
|
||||
newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
function regenerateToken() {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
async function regenerateToken() {
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
os.api('i/regenerate-token', {
|
||||
password: password,
|
||||
});
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue