allow tokens to limit a user's rank

This commit is contained in:
Hazelnoot 2025-06-21 09:46:31 -04:00
parent a4c816d07c
commit 70b85e5215
8 changed files with 72 additions and 4 deletions

22
locales/index.d.ts vendored
View file

@ -13511,6 +13511,28 @@ export interface Locale extends ILocale {
* Permissions * Permissions
*/ */
"permissions": string; "permissions": string;
/**
* Override rank
*/
"overrideRank": string;
/**
* Overrides the user rank (admin, moderator, or user) for apps using this token.
*/
"overrideRankDescription": string;
"_ranks": {
/**
* Admin
*/
"admin": string;
/**
* Moderator
*/
"mod": string;
/**
* User
*/
"user": string;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -8,6 +8,9 @@ import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiApp } from './App.js'; import { MiApp } from './App.js';
export const accessTokenRanks = ['user', 'mod', 'admin'] as const;
export type AccessTokenRank = typeof accessTokenRanks[number];
@Entity('access_token') @Entity('access_token')
export class MiAccessToken { export class MiAccessToken {
@PrimaryColumn(id()) @PrimaryColumn(id())
@ -87,4 +90,11 @@ export class MiAccessToken {
default: false, default: false,
}) })
public fetched: boolean; public fetched: boolean;
@Column('enum', {
enum: accessTokenRanks,
nullable: true,
comment: 'Limits the user\' rank (user, moderator, or admin) when using this token. If null (default), then uses the user\'s actual rank.',
})
public rank: AccessTokenRank | null;
} }

View file

@ -60,6 +60,7 @@ export const paramDef = {
grantees: { type: 'array', uniqueItems: true, items: { grantees: { type: 'array', uniqueItems: true, items: {
type: 'string', type: 'string',
} }, } },
rank: { type: 'string', enum: ['admin', 'mod', 'user'], nullable: true },
}, },
required: ['session', 'permission'], required: ['session', 'permission'],
} as const; } as const;
@ -109,6 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
description: ps.description, description: ps.description,
iconUrl: ps.iconUrl, iconUrl: ps.iconUrl,
permission: ps.permission, permission: ps.permission,
rank: ps.rank,
}); });
// Insert shared access grants // Insert shared access grants

View file

@ -28,6 +28,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</div> </div>
<MkFolder v-if="$i?.isAdmin || $i?.isModerator">
<template #label>{{ i18n.ts.overrideRank }}</template>
<template #caption>{{ i18n.ts.overrideRankDescription }}</template>
<MkSelect v-if="$i?.isAdmin" v-model="rank">
<option value="admin">{{ i18n.ts._ranks.admin }}</option>
<option value="mod">{{ i18n.ts._ranks.mod }}</option>
<option value="user">{{ i18n.ts._ranks.user }}</option>
</MkSelect>
<MkSelect v-else v-model="rank">
<option value="mod">{{ i18n.ts._ranks.mod }}</option>
<option value="user">{{ i18n.ts._ranks.user }}</option>
</MkSelect>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts.permission }}</template> <template #label>{{ i18n.ts.permission }}</template>
<template #caption>{{ i18n.ts.permissionsDescription }}</template> <template #caption>{{ i18n.ts.permissionsDescription }}</template>
@ -82,9 +98,10 @@ import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { iAmAdmin } from '@/i.js'; import { $i, iAmAdmin } from '@/i.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os'; import * as os from '@/os';
import { instance } from '@/instance'; import { instance } from '@/instance';
@ -102,7 +119,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
(ev: 'done', result: { name: string | null, permissions: string[], grantees: string[] }): void; (ev: 'done', result: { name: string | null, permissions: string[], grantees: string[], rank: string }): void;
}>(); }>();
const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin')); const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
@ -113,6 +130,12 @@ const name = ref(props.initialName);
const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>); const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>); const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
const grantees = ref<Misskey.entities.User[]>([]); const grantees = ref<Misskey.entities.User[]>([]);
const rank = ref<'admin' | 'mod' | 'user'>(
$i?.isAdmin
? 'admin'
: $i?.isModerator
? 'mod'
: 'user');
if (props.initialPermissions) { if (props.initialPermissions) {
for (const kind of props.initialPermissions) { for (const kind of props.initialPermissions) {
@ -138,6 +161,7 @@ function ok(): void {
...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []), ...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []),
], ],
grantees: grantees.value.map(g => g.id), grantees: grantees.value.map(g => g.id),
rank: rank.value,
}); });
dialog.value?.close(); dialog.value?.close();
} }

View file

@ -84,12 +84,13 @@ const pagination = {
function generateToken() { function generateToken() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
done: async result => { done: async result => {
const { name, permissions, grantees } = result; const { name, permissions, grantees, rank } = result;
const { token } = await misskeyApi('miauth/gen-token', { const { token } = await misskeyApi('miauth/gen-token', {
session: null, session: null,
name: name, name: name,
permission: permissions, permission: permissions,
grantees: grantees, grantees: grantees,
rank: rank,
}); });
os.alert({ os.alert({

View file

@ -109,11 +109,12 @@ export async function authorizePlugin(plugin: Plugin) {
initialPermissions: plugin.permissions, initialPermissions: plugin.permissions,
}, { }, {
done: async result => { done: async result => {
const { name, permissions } = result; const { name, permissions, rank } = result;
const { token } = await misskeyApi('miauth/gen-token', { const { token } = await misskeyApi('miauth/gen-token', {
session: null, session: null,
name: name, name: name,
permission: permissions, permission: permissions,
rank: rank,
}); });
res(token); res(token);
}, },

View file

@ -26379,6 +26379,8 @@ export type operations = {
iconUrl?: string | null; iconUrl?: string | null;
permission: string[]; permission: string[];
grantees?: string[]; grantees?: string[];
/** @enum {string|null} */
rank?: 'admin' | 'mod' | 'user';
}; };
}; };
}; };

View file

@ -692,3 +692,9 @@ noSharedAccess: "You have not been granted shared access to any accounts"
expand: "Expand" expand: "Expand"
collapse: "Collapse" collapse: "Collapse"
permissions: "Permissions" permissions: "Permissions"
overrideRank: "Override rank"
overrideRankDescription: "Overrides the user rank (admin, moderator, or user) for apps using this token."
_ranks:
admin: "Admin"
mod: "Moderator"
user: "User"