feat: 🔒 allow admins to reset users' 2FA and passkeys

ref: https://frfsh.plus.st/notes/9hqswpwiwjaihcgo
This commit is contained in:
ThatOneCalculator 2023-07-28 17:57:32 -07:00
parent ba694065ee
commit f973b1986f
No known key found for this signature in database
GPG key ID: 8703CACD01000000
5 changed files with 158 additions and 2 deletions

View file

@ -1129,6 +1129,11 @@ removeRecipient: "Remove recipient"
removeMember: "Remove member" removeMember: "Remove member"
verifiedLink: "Verified link" verifiedLink: "Verified link"
origin: "Origin" origin: "Origin"
delete2fa: "Delete 2FA"
deletePasskeys: "Delete passkeys"
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
inputNotMatch: "Input does not match"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -65,6 +65,8 @@ import * as ep___admin_unsuspendUser from "./endpoints/admin/unsuspend-user.js";
import * as ep___admin_updateMeta from "./endpoints/admin/update-meta.js"; import * as ep___admin_updateMeta from "./endpoints/admin/update-meta.js";
import * as ep___admin_vacuum from "./endpoints/admin/vacuum.js"; import * as ep___admin_vacuum from "./endpoints/admin/vacuum.js";
import * as ep___admin_deleteAccount from "./endpoints/admin/delete-account.js"; import * as ep___admin_deleteAccount from "./endpoints/admin/delete-account.js";
import * as ep___admin_delete2fa from "./endpoints/admin/delete-2fa.js";
import * as ep___admin_deletePasskeys from "./endpoints/admin/delete-passkeys.js";
import * as ep___admin_updateUserNote from "./endpoints/admin/update-user-note.js"; import * as ep___admin_updateUserNote from "./endpoints/admin/update-user-note.js";
import * as ep___announcements from "./endpoints/announcements.js"; import * as ep___announcements from "./endpoints/announcements.js";
import * as ep___antennas_create from "./endpoints/antennas/create.js"; import * as ep___antennas_create from "./endpoints/antennas/create.js";
@ -418,6 +420,8 @@ const eps = [
["admin/update-meta", ep___admin_updateMeta], ["admin/update-meta", ep___admin_updateMeta],
["admin/vacuum", ep___admin_vacuum], ["admin/vacuum", ep___admin_vacuum],
["admin/delete-account", ep___admin_deleteAccount], ["admin/delete-account", ep___admin_deleteAccount],
["admin/delete-2fa", ep___admin_delete2fa],
["admin/delete-passkeys", ep___admin_deletePasskeys],
["admin/update-user-note", ep___admin_updateUserNote], ["admin/update-user-note", ep___admin_updateUserNote],
["announcements", ep___announcements], ["announcements", ep___announcements],
["antennas/create", ep___antennas_create], ["antennas/create", ep___antennas_create],

View file

@ -0,0 +1,40 @@
import { Users, UserProfiles } from "@/models/index.js";
import { publishMainStream } from "@/services/stream.js";
import define from "../../define.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireAdmin: true,
res: {},
} as const;
export const paramDef = {
type: "object",
properties: {
userId: { type: "string", format: "misskey:id" },
},
required: ["userId"],
} as const;
export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
await UserProfiles.update(user.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
usePasswordLessLogin: false,
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true,
});
publishMainStream(user.id, "meUpdated", iObj);
});

View file

@ -0,0 +1,42 @@
import { Users, UserProfiles, UserSecurityKeys } from "@/models/index.js";
import { publishMainStream } from "@/services/stream.js";
import define from "../../define.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireAdmin: true,
res: {},
} as const;
export const paramDef = {
type: "object",
properties: {
userId: { type: "string", format: "misskey:id" },
},
required: ["userId"],
} as const;
export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
await UserSecurityKeys.delete({
userId: user.id,
});
await UserProfiles.update(user.id, {
usePasswordLessLogin: false,
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true,
});
publishMainStream(user.id, "meUpdated", iObj);
});

View file

@ -207,7 +207,7 @@
v-if="user.host == null && iAmModerator" v-if="user.host == null && iAmModerator"
inline inline
@click="resetPassword" @click="resetPassword"
><i class="ph-key ph-bold ph-lg"></i> ><i class="ph-password ph-bold ph-lg"></i>
{{ i18n.ts.resetPassword }}</FormButton {{ i18n.ts.resetPassword }}</FormButton
> >
<FormButton <FormButton
@ -221,6 +221,23 @@
v-if="$i.isAdmin" v-if="$i.isAdmin"
inline inline
danger danger
@click="delete2fa"
><i class="ph-key ph-bold ph-lg"></i>
{{ i18n.ts.delete2fa }}</FormButton
>
<FormButton
v-if="$i.isAdmin"
inline
danger
@click="deletePasskeys"
><i class="ph-poker-chip ph-bold ph-lg"></i>
{{ i18n.ts.deletePasskeys }}</FormButton
>
<FormButton
v-if="$i.isAdmin"
inline
primary
danger
@click="deleteAccount" @click="deleteAccount"
><i class="ph-user-minus ph-bold ph-lg"></i> ><i class="ph-user-minus ph-bold ph-lg"></i>
{{ i18n.ts.deleteAccount }}</FormButton {{ i18n.ts.deleteAccount }}</FormButton
@ -543,6 +560,54 @@ async function applyDriveCapacityOverride() {
} }
} }
async function delete2fa() {
const confirm = await os.confirm({
type: "warning",
text: i18n.ts.delete2faConfirm,
});
if (confirm.canceled) return;
const typed = await os.inputText({
text: i18n.t("typeToConfirm", { x: user?.username }),
});
if (typed.canceled) return;
if (typed.result === user?.username) {
await os.apiWithDialog("admin/delete-2fa", {
userId: user.id,
});
} else {
os.alert({
type: "error",
text: i18n.ts.inputNotMatch,
});
}
}
async function deletePasskeys() {
const confirm = await os.confirm({
type: "warning",
text: i18n.ts.deletePasskeysConfirm,
});
if (confirm.canceled) return;
const typed = await os.inputText({
text: i18n.t("typeToConfirm", { x: user?.username }),
});
if (typed.canceled) return;
if (typed.result === user?.username) {
await os.apiWithDialog("admin/delete-passkeys", {
userId: user.id,
});
} else {
os.alert({
type: "error",
text: i18n.ts.inputNotMatch,
});
}
}
async function deleteAccount() { async function deleteAccount() {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: "warning", type: "warning",
@ -562,7 +627,7 @@ async function deleteAccount() {
} else { } else {
os.alert({ os.alert({
type: "error", type: "error",
text: "input not match", text: i18n.ts.inputNotMatch,
}); });
} }
} }