spec(OAuth2): クライアント情報のDiscoveryの対応していないクライアントでも認証できるように (MisskeyIO#443)
This commit is contained in:
@@ -49,6 +49,7 @@ export const DI = {
|
||||
blockingsRepository: Symbol('blockingsRepository'),
|
||||
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
||||
hashtagsRepository: Symbol('hashtagsRepository'),
|
||||
indieAuthClientsRepository: Symbol('indieAuthClientsRepository'),
|
||||
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
|
||||
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
|
||||
authSessionsRepository: Symbol('authSessionsRepository'),
|
||||
|
30
packages/backend/src/models/IndieAuthClient.ts
Normal file
30
packages/backend/src/models/IndieAuthClient.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Column, Index } from 'typeorm';
|
||||
|
||||
@Entity('indie_auth_client')
|
||||
export class MiIndieAuthClient {
|
||||
@PrimaryColumn('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true,
|
||||
})
|
||||
public name: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
array: true, length: 512, default: '{}',
|
||||
})
|
||||
public redirectUris: string[];
|
||||
}
|
@@ -5,7 +5,77 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseReportResolver, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
|
||||
import {
|
||||
MiAbuseReportResolver,
|
||||
MiAbuseUserReport,
|
||||
MiAccessToken,
|
||||
MiAd,
|
||||
MiAnnouncement,
|
||||
MiAnnouncementRead,
|
||||
MiAntenna,
|
||||
MiApp,
|
||||
MiAuthSession,
|
||||
MiAvatarDecoration,
|
||||
MiBlocking,
|
||||
MiChannel,
|
||||
MiChannelFavorite,
|
||||
MiChannelFollowing,
|
||||
MiClip,
|
||||
MiClipFavorite,
|
||||
MiClipNote,
|
||||
MiDriveFile,
|
||||
MiDriveFolder,
|
||||
MiEmoji,
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiFollowRequest,
|
||||
MiFollowing,
|
||||
MiGalleryLike,
|
||||
MiGalleryPost,
|
||||
MiHashtag,
|
||||
MiIndieAuthClient,
|
||||
MiInstance,
|
||||
MiMeta,
|
||||
MiModerationLog,
|
||||
MiMuting,
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteUnread,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiPasswordResetRequest,
|
||||
MiPoll,
|
||||
MiPollVote,
|
||||
MiPromoNote,
|
||||
MiPromoRead,
|
||||
MiRegistrationTicket,
|
||||
MiRegistryItem,
|
||||
MiRelay,
|
||||
MiRenoteMuting,
|
||||
MiRetentionAggregation,
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
MiSignin,
|
||||
MiSwSubscription,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
MiUserIp,
|
||||
MiUserKeypair,
|
||||
MiUserList,
|
||||
MiUserListFavorite,
|
||||
MiUserListMembership,
|
||||
MiUserMemo,
|
||||
MiUserNotePining,
|
||||
MiUserPending,
|
||||
MiUserProfile,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
} from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -219,6 +289,12 @@ const $hashtagsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $indieAuthClientsRepository: Provider = {
|
||||
provide: DI.indieAuthClientsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiIndieAuthClient),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $abuseUserReportsRepository: Provider = {
|
||||
provide: DI.abuseUserReportsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
|
||||
@@ -456,6 +532,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$blockingsRepository,
|
||||
$swSubscriptionsRepository,
|
||||
$hashtagsRepository,
|
||||
$indieAuthClientsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$registrationTicketsRepository,
|
||||
$authSessionsRepository,
|
||||
@@ -526,6 +603,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$blockingsRepository,
|
||||
$swSubscriptionsRepository,
|
||||
$hashtagsRepository,
|
||||
$indieAuthClientsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$registrationTicketsRepository,
|
||||
$authSessionsRepository,
|
||||
|
@@ -27,6 +27,7 @@ import { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
import { MiGalleryPost } from '@/models/GalleryPost.js';
|
||||
import { MiHashtag } from '@/models/Hashtag.js';
|
||||
import { MiIndieAuthClient } from '@/models/IndieAuthClient.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
@@ -98,6 +99,7 @@ export {
|
||||
MiGalleryLike,
|
||||
MiGalleryPost,
|
||||
MiHashtag,
|
||||
MiIndieAuthClient,
|
||||
MiInstance,
|
||||
MiMeta,
|
||||
MiModerationLog,
|
||||
@@ -168,6 +170,7 @@ export type FollowRequestsRepository = Repository<MiFollowRequest>;
|
||||
export type GalleryLikesRepository = Repository<MiGalleryLike>;
|
||||
export type GalleryPostsRepository = Repository<MiGalleryPost>;
|
||||
export type HashtagsRepository = Repository<MiHashtag>;
|
||||
export type IndieAuthClientsRepository = Repository<MiIndieAuthClient>;
|
||||
export type InstancesRepository = Repository<MiInstance>;
|
||||
export type MetasRepository = Repository<MiMeta>;
|
||||
export type ModerationLogsRepository = Repository<MiModerationLog>;
|
||||
|
@@ -37,6 +37,7 @@ import { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
import { MiGalleryPost } from '@/models/GalleryPost.js';
|
||||
import { MiHashtag } from '@/models/Hashtag.js';
|
||||
import { MiIndieAuthClient } from '@/models/IndieAuthClient.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
@@ -172,6 +173,7 @@ export const entities = [
|
||||
MiPollVote,
|
||||
MiEmoji,
|
||||
MiHashtag,
|
||||
MiIndieAuthClient,
|
||||
MiSwSubscription,
|
||||
MiAbuseUserReport,
|
||||
MiRegistrationTicket,
|
||||
|
@@ -51,6 +51,10 @@ import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federat
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
||||
import * as ep___admin_indieAuth_create from './endpoints/admin/indie-auth/create.js';
|
||||
import * as ep___admin_indieAuth_delete from './endpoints/admin/indie-auth/delete.js';
|
||||
import * as ep___admin_indieAuth_list from './endpoints/admin/indie-auth/list.js';
|
||||
import * as ep___admin_indieAuth_update from './endpoints/admin/indie-auth/update.js';
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
@@ -428,6 +432,10 @@ const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federati
|
||||
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
|
||||
const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
|
||||
const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default };
|
||||
const $admin_indieAuth_create: Provider = { provide: 'ep:admin/indie-auth/create', useClass: ep___admin_indieAuth_create.default };
|
||||
const $admin_indieAuth_delete: Provider = { provide: 'ep:admin/indie-auth/delete', useClass: ep___admin_indieAuth_delete.default };
|
||||
const $admin_indieAuth_list: Provider = { provide: 'ep:admin/indie-auth/list', useClass: ep___admin_indieAuth_list.default };
|
||||
const $admin_indieAuth_update: Provider = { provide: 'ep:admin/indie-auth/update', useClass: ep___admin_indieAuth_update.default };
|
||||
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
|
||||
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
|
||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||
@@ -809,6 +817,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
$admin_federation_removeAllFollowing,
|
||||
$admin_federation_updateInstance,
|
||||
$admin_indieAuth_create,
|
||||
$admin_indieAuth_delete,
|
||||
$admin_indieAuth_list,
|
||||
$admin_indieAuth_update,
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
@@ -1184,6 +1196,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
$admin_federation_removeAllFollowing,
|
||||
$admin_federation_updateInstance,
|
||||
$admin_indieAuth_create,
|
||||
$admin_indieAuth_delete,
|
||||
$admin_indieAuth_list,
|
||||
$admin_indieAuth_update,
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
|
@@ -51,6 +51,10 @@ import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federat
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
||||
import * as ep___admin_indieAuth_create from './endpoints/admin/indie-auth/create.js';
|
||||
import * as ep___admin_indieAuth_delete from './endpoints/admin/indie-auth/delete.js';
|
||||
import * as ep___admin_indieAuth_list from './endpoints/admin/indie-auth/list.js';
|
||||
import * as ep___admin_indieAuth_update from './endpoints/admin/indie-auth/update.js';
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
@@ -426,6 +430,10 @@ const eps = [
|
||||
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
|
||||
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
|
||||
['admin/federation/update-instance', ep___admin_federation_updateInstance],
|
||||
['admin/indie-auth/create', ep___admin_indieAuth_create],
|
||||
['admin/indie-auth/delete', ep___admin_indieAuth_delete],
|
||||
['admin/indie-auth/list', ep___admin_indieAuth_list],
|
||||
['admin/indie-auth/update', ep___admin_indieAuth_update],
|
||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||
['admin/get-table-stats', ep___admin_getTableStats],
|
||||
['admin/get-user-ips', ep___admin_getUserIps],
|
||||
|
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { IndieAuthClientsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
redirectUris: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 1 },
|
||||
name: { type: 'string', nullable: true },
|
||||
redirectUris: {
|
||||
type: 'array', minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.indieAuthClientsRepository)
|
||||
private indieAuthClientsRepository: IndieAuthClientsRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const indieAuthClient = await this.indieAuthClientsRepository.insert({
|
||||
id: ps.id,
|
||||
createdAt: new Date(),
|
||||
name: ps.name,
|
||||
redirectUris: ps.redirectUris,
|
||||
}).then(r => this.indieAuthClientsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
|
||||
|
||||
this.moderationLogService.log(me, 'createIndieAuthClient', {
|
||||
clientId: indieAuthClient.id,
|
||||
client: indieAuthClient,
|
||||
});
|
||||
|
||||
return {
|
||||
id: indieAuthClient.id,
|
||||
createdAt: indieAuthClient.createdAt.toISOString(),
|
||||
name: indieAuthClient.name,
|
||||
redirectUris: indieAuthClient.redirectUris,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { IndieAuthClientsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
errors: {
|
||||
noSuchIndieAuthClient: {
|
||||
message: 'No such client',
|
||||
code: 'NO_SUCH_CLIENT',
|
||||
id: '02c4e690-af0c-4dc9-9f2f-c436c3b2782d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
},
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.indieAuthClientsRepository)
|
||||
private indieAuthClientsRepository: IndieAuthClientsRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const client = await this.indieAuthClientsRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (client == null) throw new ApiError(meta.errors.noSuchIndieAuthClient);
|
||||
|
||||
await this.indieAuthClientsRepository.delete(client.id);
|
||||
|
||||
this.moderationLogService.log(me, 'deleteIndieAuthClient', {
|
||||
clientId: client.id,
|
||||
client: client,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { IndieAuthClientsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:indie-auth',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
redirectUris: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.indieAuthClientsRepository)
|
||||
private indieAuthClientsRepository: IndieAuthClientsRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.indieAuthClientsRepository.createQueryBuilder('client');
|
||||
const clients = await query.offset(ps.offset).limit(ps.limit).getMany();
|
||||
|
||||
return clients.map(client => ({
|
||||
id: client.id,
|
||||
createdAt: client.createdAt.toISOString(),
|
||||
name: client.name,
|
||||
redirectUris: client.redirectUris,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { IndieAuthClientsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
errors: {
|
||||
noSuchIndieAuthClient: {
|
||||
message: 'No such client',
|
||||
code: 'NO_SUCH_CLIENT',
|
||||
id: 'd4f9440a-45aa-495c-af66-b4d1e339d4fc',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 1 },
|
||||
name: { type: 'string', nullable: true },
|
||||
redirectUris: {
|
||||
type: 'array', minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.indieAuthClientsRepository)
|
||||
private indieAuthClientsRepository: IndieAuthClientsRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const client = await this.indieAuthClientsRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (client == null) throw new ApiError(meta.errors.noSuchIndieAuthClient);
|
||||
|
||||
await this.indieAuthClientsRepository.update(client.id, {
|
||||
name: ps.name,
|
||||
redirectUris: ps.redirectUris,
|
||||
});
|
||||
|
||||
const updatedClient = await this.indieAuthClientsRepository.findOneByOrFail({ id: client.id });
|
||||
|
||||
this.moderationLogService.log(me, 'updateIndieAuthClient', {
|
||||
clientId: client.id,
|
||||
before: client,
|
||||
after: updatedClient,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -32,7 +32,12 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { AccessTokensRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type {
|
||||
AccessTokensRepository,
|
||||
IndieAuthClientsRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository
|
||||
} from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
@@ -100,8 +105,8 @@ function validateClientId(raw: string): URL {
|
||||
|
||||
interface ClientInformation {
|
||||
id: string;
|
||||
redirectUris: string[];
|
||||
name: string;
|
||||
redirectUris: string[];
|
||||
}
|
||||
|
||||
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||
@@ -246,6 +251,8 @@ export class OAuth2ProviderService {
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.accessTokensRepository)
|
||||
private accessTokensRepository: AccessTokensRepository,
|
||||
@Inject(DI.indieAuthClientsRepository)
|
||||
private indieAuthClientsRepository: IndieAuthClientsRepository,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
@@ -423,8 +430,10 @@ export class OAuth2ProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
// Find client information from the database.
|
||||
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
|
||||
// Find client information from the remote.
|
||||
const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
|
||||
const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
|
||||
|
||||
// Require the redirect URI to be included in an explicit list, per
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
|
||||
|
@@ -76,6 +76,9 @@ export const moderationLogTypes = [
|
||||
'createAd',
|
||||
'updateAd',
|
||||
'deleteAd',
|
||||
'createIndieAuthClient',
|
||||
'updateIndieAuthClient',
|
||||
'deleteIndieAuthClient',
|
||||
'createAvatarDecoration',
|
||||
'updateAvatarDecoration',
|
||||
'deleteAvatarDecoration',
|
||||
@@ -242,6 +245,19 @@ export type ModerationLogPayloads = {
|
||||
adId: string;
|
||||
ad: any;
|
||||
};
|
||||
createIndieAuthClient: {
|
||||
clientId: string;
|
||||
client: any;
|
||||
};
|
||||
updateIndieAuthClient: {
|
||||
clientId: string;
|
||||
before: any;
|
||||
after: any;
|
||||
};
|
||||
deleteIndieAuthClient: {
|
||||
clientId: string;
|
||||
client: any;
|
||||
};
|
||||
createAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
avatarDecoration: any;
|
||||
|
Reference in New Issue
Block a user