diff --git a/locales/index.d.ts b/locales/index.d.ts index 2d75a86ca6..e8b5ddd59c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10434,6 +10434,18 @@ export interface Locale extends ILocale { * Import of {x} has been completed */ "importOfXCompleted": ParameterizedString<"x">; + /** + * Shared access granted + */ + "sharedAccessGranted": string; + /** + * Shared access revoked + */ + "sharedAccessRevoked": string; + /** + * Shared access login + */ + "sharedAccessLogin": string; }; "_deck": { /** @@ -13536,7 +13548,39 @@ export interface Locale extends ILocale { * User */ "user": string; + /** + * default + */ + "default": string; }; + /** + * Permissions: {num} + */ + "permissionsLabel": ParameterizedString<"num">; + /** + * You have been granted shared access to {target} with {rank} rank and {perms} permissions. + */ + "sharedAccessGranted": ParameterizedString<"target" | "rank" | "perms">; + /** + * Shared access to {target} has been revoked. + */ + "sharedAccessRevoked": ParameterizedString<"target">; + /** + * {target} logged in via shared access. + */ + "sharedAccessLogin": ParameterizedString<"target">; + /** + * Unique name to record the purpose of this access token + */ + "accessTokenNameDescription": string; + /** + * Are you sure you want to revoke this token? + */ + "confirmRevokeToken": string; + /** + * Are you sure you want to revoke this token? {num} shared other users will lose shared access. + */ + "confirmRevokeSharedToken": ParameterizedString<"num">; } declare const locales: { [lang: string]: Locale; diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 8b35fd9f38..f56337a34c 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { In, EntityNotFoundError } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository, MiAccessToken } from '@/models/_.js'; +import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository, MiAccessToken, AccessTokensRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; import type { MiNote } from '@/models/Note.js'; @@ -21,7 +21,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNotePosted']); function undefOnMissing(packPromise: Promise): Promise { return packPromise.catch(err => { @@ -49,6 +49,9 @@ export class NotificationEntityService implements OnModuleInit { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, + @Inject(DI.accessTokensRepository) + private readonly accessTokensRepository: AccessTokensRepository, + private cacheService: CacheService, ) { } @@ -162,6 +165,9 @@ export class NotificationEntityService implements OnModuleInit { return null; } + const needsAccessToken = notification.type === 'sharedAccessGranted'; + const accessToken = (needsAccessToken && notification.tokenId) ? await this.accessTokensRepository.findOneBy({ id: notification.tokenId }) : null; + return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -200,6 +206,14 @@ export class NotificationEntityService implements OnModuleInit { header: notification.customHeader, icon: notification.customIcon, } : {}), + ...(notification.type === 'sharedAccessGranted' ? { + tokenId: notification.tokenId, + token: accessToken ? { + id: accessToken.id, + permission: accessToken.permission, + rank: accessToken.rank, + } : null, + } : {}), }); } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 1f87c1068d..dbdaad7495 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -152,6 +152,22 @@ export type MiNotification = { id: string; createdAt: string; noteId: MiNote['id']; +} | { + type: 'sharedAccessGranted'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + tokenId: MiAccessToken['id']; +} | { + type: 'sharedAccessRevoked'; + id: string; + createdAt: string; + notifierId: MiUser['id']; +} | { + type: 'sharedAccessLogin'; + id: string; + createdAt: string; + notifierId: MiUser['id']; }; export type MiGroupedNotification = MiNotification | { diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 2663b2f5f8..f314cec63f 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -460,6 +460,88 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['sharedAccessGranted'], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + token: { + type: 'object', + optional: false, nullable: true, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + permission: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + rank: { + type: 'string', + enum: ['admin', 'mod', 'user'], + optional: false, nullable: true, + }, + }, + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['sharedAccessRevoked'], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['sharedAccessLogin'], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 2a8e037e61..cbe482a821 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { requireCredential: true, @@ -37,29 +38,37 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, + + private readonly notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { if (ps.tokenId) { - const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } }); + const tokenExist = await this.accessTokensRepository.findOne({ where: { id: ps.tokenId } }); if (tokenExist) { + for (const granteeId of tokenExist.granteeIds) { + this.notificationService.createNotification(granteeId, 'sharedAccessRevoked', {}, me.id); + } + await this.accessTokensRepository.delete({ id: ps.tokenId, userId: me.id, }); } } else if (ps.token) { - const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } }); + const tokenExist = await this.accessTokensRepository.findOne({ where: { token: ps.token } }); if (tokenExist) { + for (const granteeId of tokenExist.granteeIds) { + this.notificationService.createNotification(granteeId, 'sharedAccessRevoked', {}, me.id); + } + await this.accessTokensRepository.delete({ token: ps.token, userId: me.id, }); } } - - // TODO notify of access revoked }); } } diff --git a/packages/backend/src/server/api/endpoints/i/shared-access/login.ts b/packages/backend/src/server/api/endpoints/i/shared-access/login.ts index e2a6b3305f..7a624cd6ce 100644 --- a/packages/backend/src/server/api/endpoints/i/shared-access/login.ts +++ b/packages/backend/src/server/api/endpoints/i/shared-access/login.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository } from '@/models/_.js'; import { ApiError } from '@/server/api/error.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { requireCredential: true, @@ -57,6 +58,8 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.accessTokensRepository) private readonly accessTokensRepository: AccessTokensRepository, + + private readonly notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { const token = await this.accessTokensRepository.findOneBy({ id: ps.grantId }); @@ -69,7 +72,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchAccess); } - // TODO notify of login + this.notificationService.createNotification(token.userId, 'sharedAccessLogin', {}, me.id); return { token: token.token, diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index fcfed8b7a9..d1ac9db505 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -117,7 +117,11 @@ export default class extends Endpoint { // eslint- granteeIds: ps.grantees, }); - // TODO notify of access granted + if (ps.grantees) { + for (const granteeId of ps.grantees) { + this.notificationService.createNotification(granteeId, 'sharedAccessGranted', { tokenId: accessTokenId }, me.id); + } + } // アクセストークンが生成されたことを通知 this.notificationService.createNotification(me.id, 'createToken', {}); diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index bd15dab26d..edbf98bd29 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only