basic support for Shared Access Accounts
This commit is contained in:
parent
7715f36a99
commit
fa5a46f379
22 changed files with 789 additions and 32 deletions
48
locales/index.d.ts
vendored
48
locales/index.d.ts
vendored
|
|
@ -13459,6 +13459,54 @@ export interface Locale extends ILocale {
|
||||||
* Hide ads
|
* Hide ads
|
||||||
*/
|
*/
|
||||||
"hideAds": string;
|
"hideAds": string;
|
||||||
|
/**
|
||||||
|
* Apps using this token will only have access to the functions listed below.
|
||||||
|
*/
|
||||||
|
"permissionsDescription": string;
|
||||||
|
/**
|
||||||
|
* Shared account
|
||||||
|
*/
|
||||||
|
"sharedAccount": string;
|
||||||
|
/**
|
||||||
|
* Shared access
|
||||||
|
*/
|
||||||
|
"sharedAccess": string;
|
||||||
|
/**
|
||||||
|
* Any accounts listed here will be granted access to the token and may use it to access this account.
|
||||||
|
*/
|
||||||
|
"sharedAccessDescription": string;
|
||||||
|
/**
|
||||||
|
* Share access
|
||||||
|
*/
|
||||||
|
"addGrantee": string;
|
||||||
|
/**
|
||||||
|
* Remove access
|
||||||
|
*/
|
||||||
|
"removeGrantee": string;
|
||||||
|
/**
|
||||||
|
* Login with shared access
|
||||||
|
*/
|
||||||
|
"loginWithSharedAccess": string;
|
||||||
|
/**
|
||||||
|
* Login with granted access to a shared account
|
||||||
|
*/
|
||||||
|
"loginWithSharedAccessDescription": string;
|
||||||
|
/**
|
||||||
|
* You have not been granted shared access to any accounts
|
||||||
|
*/
|
||||||
|
"noSharedAccess": string;
|
||||||
|
/**
|
||||||
|
* Expand
|
||||||
|
*/
|
||||||
|
"expand": string;
|
||||||
|
/**
|
||||||
|
* Collapse
|
||||||
|
*/
|
||||||
|
"collapse": string;
|
||||||
|
/**
|
||||||
|
* Permissions
|
||||||
|
*/
|
||||||
|
"permissions": string;
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CreateSharedAccessToken1750478202328 {
|
||||||
|
name = 'CreateSharedAccessToken1750478202328'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "shared_access_token" ("accessTokenId" character varying(32) NOT NULL, "granteeId" character varying(32) NOT NULL, CONSTRAINT "PK_b741ebcd3988295f4140a9f31b4" PRIMARY KEY ("accessTokenId")); COMMENT ON COLUMN "shared_access_token"."accessTokenId" IS 'ID of the access token that is shared'; COMMENT ON COLUMN "shared_access_token"."granteeId" IS 'ID of the user who is allowed to use this access token'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_shared_access_token_granteeId" ON "shared_access_token" ("granteeId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_access_token" ADD CONSTRAINT "FK_shared_access_token_accessTokenId" FOREIGN KEY ("accessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_access_token" ADD CONSTRAINT "FK_shared_access_token_granteeId" FOREIGN KEY ("granteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_access_token" DROP CONSTRAINT "FK_shared_access_token_granteeId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "shared_access_token" DROP CONSTRAINT "FK_shared_access_token_accessTokenId"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_shared_access_token_granteeId"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "shared_access_token"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -96,5 +96,6 @@ export const DI = {
|
||||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||||
noteScheduleRepository: Symbol('noteScheduleRepository'),
|
noteScheduleRepository: Symbol('noteScheduleRepository'),
|
||||||
|
sharedAccessToken: Symbol('sharedAccessToken'),
|
||||||
//#endregion
|
//#endregion
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ import {
|
||||||
SkApContext,
|
SkApContext,
|
||||||
SkApFetchLog,
|
SkApFetchLog,
|
||||||
SkApInboxLog,
|
SkApInboxLog,
|
||||||
|
SkSharedAccessToken,
|
||||||
} from './_.js';
|
} from './_.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
|
|
@ -152,6 +153,12 @@ const $apInboxLogsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $skSharedAccessToken: Provider = {
|
||||||
|
provide: DI.sharedAccessToken,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(SkSharedAccessToken).extend(miRepository as MiRepository<SkSharedAccessToken>),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $noteFavoritesRepository: Provider = {
|
const $noteFavoritesRepository: Provider = {
|
||||||
provide: DI.noteFavoritesRepository,
|
provide: DI.noteFavoritesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
|
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
|
||||||
|
|
@ -585,6 +592,7 @@ const $noteScheduleRepository: Provider = {
|
||||||
$apContextRepository,
|
$apContextRepository,
|
||||||
$apFetchLogsRepository,
|
$apFetchLogsRepository,
|
||||||
$apInboxLogsRepository,
|
$apInboxLogsRepository,
|
||||||
|
$skSharedAccessToken,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
|
@ -667,6 +675,7 @@ const $noteScheduleRepository: Provider = {
|
||||||
$apContextRepository,
|
$apContextRepository,
|
||||||
$apFetchLogsRepository,
|
$apFetchLogsRepository,
|
||||||
$apInboxLogsRepository,
|
$apInboxLogsRepository,
|
||||||
|
$skSharedAccessToken,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
|
|
||||||
51
packages/backend/src/models/SkSharedAccessToken.ts
Normal file
51
packages/backend/src/models/SkSharedAccessToken.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
|
import { id } from '@/models/util/id.js';
|
||||||
|
import { MiUser } from '@/models/User.js';
|
||||||
|
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||||
|
|
||||||
|
@Entity('shared_access_token')
|
||||||
|
export class SkSharedAccessToken {
|
||||||
|
@PrimaryColumn({
|
||||||
|
...id(),
|
||||||
|
comment: 'ID of the access token that is shared',
|
||||||
|
})
|
||||||
|
public accessTokenId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => MiAccessToken, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'accessTokenId',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
foreignKeyConstraintName: 'FK_shared_access_token_accessTokenId',
|
||||||
|
})
|
||||||
|
public accessToken: MiAccessToken;
|
||||||
|
|
||||||
|
@Index('IDX_shared_access_token_granteeId')
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'ID of the user who is allowed to use this access token',
|
||||||
|
})
|
||||||
|
public granteeId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => MiUser, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'granteeId',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
foreignKeyConstraintName: 'FK_shared_access_token_granteeId',
|
||||||
|
})
|
||||||
|
public grantee?: MiUser;
|
||||||
|
|
||||||
|
constructor(props?: Partial<SkSharedAccessToken>) {
|
||||||
|
if (props) {
|
||||||
|
Object.assign(this, props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,6 +98,7 @@ import { SkApInboxLog } from '@/models/SkApInboxLog.js';
|
||||||
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
||||||
import { SkApContext } from '@/models/SkApContext.js';
|
import { SkApContext } from '@/models/SkApContext.js';
|
||||||
import { SkLatestNote } from '@/models/LatestNote.js';
|
import { SkLatestNote } from '@/models/LatestNote.js';
|
||||||
|
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||||
|
|
||||||
export interface MiRepository<T extends ObjectLiteral> {
|
export interface MiRepository<T extends ObjectLiteral> {
|
||||||
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
|
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
|
||||||
|
|
@ -168,6 +169,7 @@ export {
|
||||||
SkApContext,
|
SkApContext,
|
||||||
SkApFetchLog,
|
SkApFetchLog,
|
||||||
SkApInboxLog,
|
SkApInboxLog,
|
||||||
|
SkSharedAccessToken,
|
||||||
MiAbuseUserReport,
|
MiAbuseUserReport,
|
||||||
MiAbuseReportNotificationRecipient,
|
MiAbuseReportNotificationRecipient,
|
||||||
MiAccessToken,
|
MiAccessToken,
|
||||||
|
|
@ -327,4 +329,5 @@ export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<
|
||||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||||
export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>;
|
export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>;
|
||||||
export type NoteScheduleRepository = Repository<MiNoteSchedule>;
|
export type NoteScheduleRepository = Repository<MiNoteSchedule> & MiRepository<MiNoteSchedule>;
|
||||||
|
export type SharedAccessTokensRepository = Repository<SkSharedAccessToken> & MiRepository<SkSharedAccessToken>;
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ import { SkLatestNote } from '@/models/LatestNote.js';
|
||||||
import { SkApContext } from '@/models/SkApContext.js';
|
import { SkApContext } from '@/models/SkApContext.js';
|
||||||
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
||||||
import { SkApInboxLog } from '@/models/SkApInboxLog.js';
|
import { SkApInboxLog } from '@/models/SkApInboxLog.js';
|
||||||
|
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||||
|
|
||||||
pg.types.setTypeParser(20, Number);
|
pg.types.setTypeParser(20, Number);
|
||||||
|
|
||||||
|
|
@ -213,6 +214,7 @@ export const entities = [
|
||||||
SkApContext,
|
SkApContext,
|
||||||
SkApFetchLog,
|
SkApFetchLog,
|
||||||
SkApInboxLog,
|
SkApInboxLog,
|
||||||
|
SkSharedAccessToken,
|
||||||
MiAnnouncement,
|
MiAnnouncement,
|
||||||
MiAnnouncementRead,
|
MiAnnouncementRead,
|
||||||
MiMeta,
|
MiMeta,
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,8 @@ export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-
|
||||||
export * as 'i/registry/set' from './endpoints/i/registry/set.js';
|
export * as 'i/registry/set' from './endpoints/i/registry/set.js';
|
||||||
export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
|
export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
|
||||||
export * as 'i/signin-history' from './endpoints/i/signin-history.js';
|
export * as 'i/signin-history' from './endpoints/i/signin-history.js';
|
||||||
|
export * as 'i/shared-access/list' from './endpoints/i/shared-access/list.js';
|
||||||
|
export * as 'i/shared-access/login' from './endpoints/i/shared-access/login.js';
|
||||||
export * as 'i/unpin' from './endpoints/i/unpin.js';
|
export * as 'i/unpin' from './endpoints/i/unpin.js';
|
||||||
export * as 'i/update' from './endpoints/i/update.js';
|
export * as 'i/update' from './endpoints/i/update.js';
|
||||||
export * as 'i/update-email' from './endpoints/i/update-email.js';
|
export * as 'i/update-email' from './endpoints/i/update-email.js';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { SharedAccessTokensRepository } from '@/models/_.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
secure: true,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.sharedAccessToken)
|
||||||
|
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||||
|
|
||||||
|
private readonly userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const tokens = await this.sharedAccessTokensRepository.find({
|
||||||
|
where: { granteeId: me.id },
|
||||||
|
relations: { accessToken: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = tokens.map(token => token.accessToken!.userId);
|
||||||
|
const packedUsers: Packed<'UserLite'>[] = await this.userEntityService.packMany(users, me);
|
||||||
|
const packedUserMap = new Map<string, Packed<'UserLite'>>(packedUsers.map(u => [u.id, u]));
|
||||||
|
|
||||||
|
return await Promise.all(tokens.map(async token => ({
|
||||||
|
id: token.accessTokenId,
|
||||||
|
permissions: token.accessToken!.permission,
|
||||||
|
user: packedUserMap.get(token.accessToken!.userId) as Packed<'UserLite'>,
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { SharedAccessTokensRepository } from '@/models/_.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
secure: true,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchAccess: {
|
||||||
|
message: 'No such access',
|
||||||
|
code: 'NO_SUCH_ACCESS',
|
||||||
|
id: 'd536e0f2-47fc-4d66-843c-f9276e98030f',
|
||||||
|
httpStatusCode: 403,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
grantId: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['grantId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.sharedAccessToken)
|
||||||
|
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const token = await this.sharedAccessTokensRepository.findOne({
|
||||||
|
where: { accessTokenId: ps.grantId, granteeId: me.id },
|
||||||
|
relations: { accessToken: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(meta.errors.noSuchAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
token: token.accessToken!.token,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
userId: token.accessToken!.userId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
|
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AccessTokensRepository } from '@/models/_.js';
|
import type { AccessTokensRepository, SharedAccessTokensRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
|
@ -29,6 +32,14 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// 10 calls per 5 seconds
|
// 10 calls per 5 seconds
|
||||||
limit: {
|
limit: {
|
||||||
duration: 1000 * 5,
|
duration: 1000 * 5,
|
||||||
|
|
@ -46,6 +57,9 @@ export const paramDef = {
|
||||||
permission: { type: 'array', uniqueItems: true, items: {
|
permission: { type: 'array', uniqueItems: true, items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
grantees: { type: 'array', uniqueItems: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
} },
|
||||||
},
|
},
|
||||||
required: ['session', 'permission'],
|
required: ['session', 'permission'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
@ -56,18 +70,36 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.accessTokensRepository)
|
@Inject(DI.accessTokensRepository)
|
||||||
private accessTokensRepository: AccessTokensRepository,
|
private accessTokensRepository: AccessTokensRepository,
|
||||||
|
|
||||||
|
@Inject(DI.sharedAccessToken)
|
||||||
|
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.db)
|
||||||
|
private readonly db: DataSource,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
if (ps.grantees && ps.grantees.length > 0) {
|
||||||
|
const existingCount = await this.usersRepository.countBy({ id: In(ps.grantees) });
|
||||||
|
if (existingCount !== ps.grantees.length) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate access token
|
// Generate access token
|
||||||
const accessToken = secureRndstr(32);
|
const accessToken = secureRndstr(32);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const accessTokenId = this.idService.gen(now.getTime());
|
||||||
|
|
||||||
|
await this.db.transaction(async tem => {
|
||||||
// Insert access token doc
|
// Insert access token doc
|
||||||
await this.accessTokensRepository.insert({
|
await this.accessTokensRepository.insert({
|
||||||
id: this.idService.gen(now.getTime()),
|
id: accessTokenId,
|
||||||
lastUsedAt: now,
|
lastUsedAt: now,
|
||||||
session: ps.session,
|
session: ps.session,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
@ -79,6 +111,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
permission: ps.permission,
|
permission: ps.permission,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Insert shared access grants
|
||||||
|
if (ps.grantees && ps.grantees.length > 0) {
|
||||||
|
const grants = ps.grantees.map(granteeId => new SkSharedAccessToken({ accessTokenId, granteeId }));
|
||||||
|
await this.sharedAccessTokensRepository.insert(grants);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO notify of access granted
|
||||||
|
|
||||||
// アクセストークンが生成されたことを通知
|
// アクセストークンが生成されたことを通知
|
||||||
this.notificationService.createNotification(me.id, 'createToken', {});
|
this.notificationService.createNotification(me.id, 'createToken', {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { prefer } from '@/preferences.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { signout } from '@/signout.js';
|
import { signout } from '@/signout.js';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||||
|
|
||||||
|
|
@ -299,6 +300,15 @@ export async function openAccountMenu(opts: {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.sharedAccess,
|
||||||
|
action: () => {
|
||||||
|
getAccountWithSharedAccessDialog().then((res) => {
|
||||||
|
if (res != null) {
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
|
@ -324,6 +334,24 @@ export async function openAccountMenu(opts: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAccountWithSharedAccessDialog(): Promise<{ id: string, token: string } | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/SkSigninSharedAccessDialog.vue')), {}, {
|
||||||
|
done: async (res: { id: string, i: string }) => {
|
||||||
|
const user = await fetchAccount(res.i, res.id, true);
|
||||||
|
await addAccount(host, user, res.i);
|
||||||
|
resolve({ id: res.id, token: res.i });
|
||||||
|
},
|
||||||
|
cancelled: () => {
|
||||||
|
resolve(null);
|
||||||
|
},
|
||||||
|
closed: () => {
|
||||||
|
dispose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
|
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkModalWindow
|
<MkModalWindow
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="400"
|
:width="500"
|
||||||
:height="450"
|
:height="600"
|
||||||
:withOkButton="true"
|
:withOkButton="true"
|
||||||
:okButtonDisabled="false"
|
:okButtonDisabled="false"
|
||||||
:canClose="false"
|
:canClose="false"
|
||||||
|
|
@ -27,7 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.name }}</template>
|
<template #label>{{ i18n.ts.name }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</div>
|
</div>
|
||||||
<div><b>{{ i18n.ts.permission }}</b></div>
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #label>{{ i18n.ts.permission }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.permissionsDescription }}</template>
|
||||||
|
|
||||||
<div class="_buttons">
|
<div class="_buttons">
|
||||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||||
|
|
@ -41,6 +45,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #label>{{ i18n.ts.sharedAccess }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.sharedAccessDescription }}</template>
|
||||||
|
|
||||||
|
<MkButton primary @click="addGrantee">
|
||||||
|
<i class="ti ti-plus"></i> {{ i18n.ts.addGrantee }}
|
||||||
|
</MkButton>
|
||||||
|
|
||||||
|
<div v-for="(grantee, i) of grantees" :key="grantee.id" :class="$style.grantee">
|
||||||
|
<MkUserCardMini :user="grantee" :withChart="false"/>
|
||||||
|
<button v-tooltip="i18n.ts.removeGrantee" class="_textButton" @click="() => removeGrantee(i)"><i class="ti ti-x"></i></button>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
|
|
@ -56,6 +75,10 @@ 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 { iAmAdmin } from '@/i.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
|
@ -71,7 +94,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[] }): void;
|
(ev: 'done', result: { name: string | null, permissions: string[], grantees: 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'));
|
||||||
|
|
@ -81,6 +104,7 @@ const dialog = useTemplateRef('dialog');
|
||||||
const name = ref(props.initialName);
|
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[]>([]);
|
||||||
|
|
||||||
if (props.initialPermissions) {
|
if (props.initialPermissions) {
|
||||||
for (const kind of props.initialPermissions) {
|
for (const kind of props.initialPermissions) {
|
||||||
|
|
@ -105,6 +129,7 @@ function ok(): void {
|
||||||
...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]),
|
...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]),
|
||||||
...(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),
|
||||||
});
|
});
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +155,18 @@ function enableAll(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addGrantee(): Promise<void> {
|
||||||
|
const user = await os.selectUser({
|
||||||
|
includeSelf: true,
|
||||||
|
localOnly: instance.noteSearchableScope === 'local',
|
||||||
|
});
|
||||||
|
grantees.value.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGrantee(index: number) {
|
||||||
|
grantees.value.splice(index, 1);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
|
|
@ -147,4 +184,10 @@ function enableAll(): void {
|
||||||
color: var(--MI_THEME-error);
|
color: var(--MI_THEME-error);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grantee {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--MI-marginHalf);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
126
packages/frontend/src/components/SkSigninSharedAccessDialog.vue
Normal file
126
packages/frontend/src/components/SkSigninSharedAccessDialog.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="modal"
|
||||||
|
:width="500"
|
||||||
|
:height="600"
|
||||||
|
:withOkButton="false"
|
||||||
|
:canClose="true"
|
||||||
|
@close="onClose"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>{{ i18n.ts.loginWithSharedAccess }}</template>
|
||||||
|
|
||||||
|
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||||
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" draggable="false" alt="no results"/>
|
||||||
|
<div>{{ i18n.ts.noSharedAccess }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ items }">
|
||||||
|
<div class="_gaps">
|
||||||
|
<div v-for="(grant, i) of items" :key="grant.id" :class="$style.grant">
|
||||||
|
<MkUserCardMini :user="grant.user" :withChart="false" :class="$style.user"/>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<button v-tooltip="i18n.ts.login" class="_textButton" @click="onLogin(grant.id)"><i class="ph-sign-in ph-bold ph-lg"></i></button>
|
||||||
|
<button v-if="isExpanded(i)" v-tooltip="i18n.ts.collapse" class="_textButton" @click="collapse(i)"><i class="ph-caret-up ph-bold ph-lg"></i></button>
|
||||||
|
<button v-else v-tooltip="i18n.ts.expand" class="_textButton" @click="expand(i)"><i class="ph-caret-down ph-bold ph-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isExpanded(i)" :class="$style.perms">
|
||||||
|
<span>{{ i18n.ts.permissions }}:</span>
|
||||||
|
<ul>
|
||||||
|
<li v-for="perm of grant.permissions" :key="perm">{{ i18n.ts._permissions[perm] ?? perm }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
|
import type { Paging } from '@/components/MkPagination.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import { infoImageUrl } from '@/instance';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { id: string, i: string }): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
(ev: 'cancelled'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagination = computed(() => ({
|
||||||
|
endpoint: 'i/shared-access/list',
|
||||||
|
params: {},
|
||||||
|
limit: 10,
|
||||||
|
} satisfies Paging));
|
||||||
|
|
||||||
|
const modal = useTemplateRef('modal');
|
||||||
|
const expandedIds = ref(new Set<number>());
|
||||||
|
|
||||||
|
function isExpanded(i: number) {
|
||||||
|
return expandedIds.value.has(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expand(i: number) {
|
||||||
|
expandedIds.value.add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapse(i: number) {
|
||||||
|
expandedIds.value.delete(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLogin(grantId: string) {
|
||||||
|
const { userId, token } = await os.apiWithDialog('i/shared-access/login', { grantId });
|
||||||
|
if (modal.value) modal.value.close();
|
||||||
|
emit('done', { id: userId, i: token });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
if (modal.value) modal.value.close();
|
||||||
|
emit('cancelled');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.grant {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--MI-marginHalf);
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perms {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--MI-marginHalf);
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perms > ul {
|
||||||
|
margin: 0 0 0 1.5em;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -24,7 +24,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
|
import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog, getAccountWithSharedAccessDialog } from '@/accounts.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
|
|
@ -59,9 +59,20 @@ function addAccount(ev: MouseEvent) {
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.createAccount,
|
text: i18n.ts.createAccount,
|
||||||
action: () => { createAccount(); },
|
action: () => { createAccount(); },
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.sharedAccount,
|
||||||
|
action: () => { addSharedAccount(); },
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSharedAccount() {
|
||||||
|
getAccountWithSharedAccessDialog().then((res) => {
|
||||||
|
if (res != null) {
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addExistingAccount() {
|
function addExistingAccount() {
|
||||||
getAccountWithSigninDialog().then((res) => {
|
getAccountWithSigninDialog().then((res) => {
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,12 @@ 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 } = result;
|
const { name, permissions, grantees } = 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
os.alert({
|
os.alert({
|
||||||
|
|
|
||||||
|
|
@ -1979,6 +1979,9 @@ declare namespace entities {
|
||||||
IRegistryScopesWithDomainResponse,
|
IRegistryScopesWithDomainResponse,
|
||||||
IRegistrySetRequest,
|
IRegistrySetRequest,
|
||||||
IRevokeTokenRequest,
|
IRevokeTokenRequest,
|
||||||
|
ISharedAccessListResponse,
|
||||||
|
ISharedAccessLoginRequest,
|
||||||
|
ISharedAccessLoginResponse,
|
||||||
ISigninHistoryRequest,
|
ISigninHistoryRequest,
|
||||||
ISigninHistoryResponse,
|
ISigninHistoryResponse,
|
||||||
IUnpinRequest,
|
IUnpinRequest,
|
||||||
|
|
@ -2765,6 +2768,15 @@ type IRevokeTokenRequest = operations['i___revoke-token']['requestBody']['conten
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIError;
|
function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIError;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ISharedAccessListResponse = operations['i___shared-access___list']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ISharedAccessLoginRequest = operations['i___shared-access___login']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ISharedAccessLoginResponse = operations['i___shared-access___login']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json'];
|
type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3459,6 +3459,30 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
request<E extends 'i/shared-access/list', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
request<E extends 'i/shared-access/login', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,9 @@ import type {
|
||||||
IRegistryScopesWithDomainResponse,
|
IRegistryScopesWithDomainResponse,
|
||||||
IRegistrySetRequest,
|
IRegistrySetRequest,
|
||||||
IRevokeTokenRequest,
|
IRevokeTokenRequest,
|
||||||
|
ISharedAccessListResponse,
|
||||||
|
ISharedAccessLoginRequest,
|
||||||
|
ISharedAccessLoginResponse,
|
||||||
ISigninHistoryRequest,
|
ISigninHistoryRequest,
|
||||||
ISigninHistoryResponse,
|
ISigninHistoryResponse,
|
||||||
IUnpinRequest,
|
IUnpinRequest,
|
||||||
|
|
@ -980,6 +983,8 @@ export type Endpoints = {
|
||||||
'i/registry/scopes-with-domain': { req: EmptyRequest; res: IRegistryScopesWithDomainResponse };
|
'i/registry/scopes-with-domain': { req: EmptyRequest; res: IRegistryScopesWithDomainResponse };
|
||||||
'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse };
|
'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse };
|
||||||
'i/revoke-token': { req: IRevokeTokenRequest; res: EmptyResponse };
|
'i/revoke-token': { req: IRevokeTokenRequest; res: EmptyResponse };
|
||||||
|
'i/shared-access/list': { req: EmptyRequest; res: ISharedAccessListResponse };
|
||||||
|
'i/shared-access/login': { req: ISharedAccessLoginRequest; res: ISharedAccessLoginResponse };
|
||||||
'i/signin-history': { req: ISigninHistoryRequest; res: ISigninHistoryResponse };
|
'i/signin-history': { req: ISigninHistoryRequest; res: ISigninHistoryResponse };
|
||||||
'i/unpin': { req: IUnpinRequest; res: IUnpinResponse };
|
'i/unpin': { req: IUnpinRequest; res: IUnpinResponse };
|
||||||
'i/update': { req: IUpdateRequest; res: IUpdateResponse };
|
'i/update': { req: IUpdateRequest; res: IUpdateResponse };
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,9 @@ export type IRegistryRemoveRequest = operations['i___registry___remove']['reques
|
||||||
export type IRegistryScopesWithDomainResponse = operations['i___registry___scopes-with-domain']['responses']['200']['content']['application/json'];
|
export type IRegistryScopesWithDomainResponse = operations['i___registry___scopes-with-domain']['responses']['200']['content']['application/json'];
|
||||||
export type IRegistrySetRequest = operations['i___registry___set']['requestBody']['content']['application/json'];
|
export type IRegistrySetRequest = operations['i___registry___set']['requestBody']['content']['application/json'];
|
||||||
export type IRevokeTokenRequest = operations['i___revoke-token']['requestBody']['content']['application/json'];
|
export type IRevokeTokenRequest = operations['i___revoke-token']['requestBody']['content']['application/json'];
|
||||||
|
export type ISharedAccessListResponse = operations['i___shared-access___list']['responses']['200']['content']['application/json'];
|
||||||
|
export type ISharedAccessLoginRequest = operations['i___shared-access___login']['requestBody']['content']['application/json'];
|
||||||
|
export type ISharedAccessLoginResponse = operations['i___shared-access___login']['responses']['200']['content']['application/json'];
|
||||||
export type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json'];
|
export type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json'];
|
||||||
export type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
|
export type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
|
||||||
export type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
|
export type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
|
||||||
|
|
|
||||||
|
|
@ -2988,6 +2988,26 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['i___revoke-token'];
|
post: operations['i___revoke-token'];
|
||||||
};
|
};
|
||||||
|
'/i/shared-access/list': {
|
||||||
|
/**
|
||||||
|
* i/shared-access/list
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
post: operations['i___shared-access___list'];
|
||||||
|
};
|
||||||
|
'/i/shared-access/login': {
|
||||||
|
/**
|
||||||
|
* i/shared-access/login
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
post: operations['i___shared-access___login'];
|
||||||
|
};
|
||||||
'/i/signin-history': {
|
'/i/signin-history': {
|
||||||
/**
|
/**
|
||||||
* i/signin-history
|
* i/signin-history
|
||||||
|
|
@ -25085,6 +25105,126 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* i/shared-access/list
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
'i___shared-access___list': {
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
id: string;
|
||||||
|
user: components['schemas']['UserLite'];
|
||||||
|
permissions: string[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Too many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* i/shared-access/login
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
'i___shared-access___login': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
grantId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
userId: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Too many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* i/signin-history
|
* i/signin-history
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
@ -26238,6 +26378,7 @@ export type operations = {
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
iconUrl?: string | null;
|
iconUrl?: string | null;
|
||||||
permission: string[];
|
permission: string[];
|
||||||
|
grantees?: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -678,3 +678,17 @@ clearCachedFilesOptions:
|
||||||
customFontSize: "Custom font size"
|
customFontSize: "Custom font size"
|
||||||
|
|
||||||
hideAds: "Hide ads"
|
hideAds: "Hide ads"
|
||||||
|
|
||||||
|
permissionsDescription: "Apps using this token will only have access to the functions listed below."
|
||||||
|
|
||||||
|
sharedAccount: "Shared account"
|
||||||
|
sharedAccess: "Shared access"
|
||||||
|
sharedAccessDescription: "Any accounts listed here will be granted access to the token and may use it to access this account."
|
||||||
|
addGrantee: "Share access"
|
||||||
|
removeGrantee: "Remove access"
|
||||||
|
loginWithSharedAccess: "Login with shared access"
|
||||||
|
loginWithSharedAccessDescription: "Login with granted access to a shared account"
|
||||||
|
noSharedAccess: "You have not been granted shared access to any accounts"
|
||||||
|
expand: "Expand"
|
||||||
|
collapse: "Collapse"
|
||||||
|
permissions: "Permissions"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue