feat: 🔒 allow admins to reset users' 2FA and passkeys
ref: https://frfsh.plus.st/notes/9hqswpwiwjaihcgo
This commit is contained in:
parent
ba694065ee
commit
f973b1986f
5 changed files with 158 additions and 2 deletions
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue