mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-27 14:28:49 +09:00
feat(SSO): JWTやSAMLでのSingle Sign-Onの実装 (MisskeyIO#519)
This commit is contained in:
parent
d300a6829f
commit
8c1db331e7
@ -60,10 +60,10 @@
|
||||
"typescript": "5.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.6.6",
|
||||
"cypress": "13.7.0",
|
||||
"eslint": "8.57.0",
|
||||
"ncp": "2.0.0",
|
||||
"start-server-and-test": "2.0.3"
|
||||
|
@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class IndieAuthClient1707697398681 {
|
||||
name = 'IndieAuthClient1707697398681'
|
||||
|
||||
|
15
packages/backend/migration/1710416761960-single-sign-on.js
Normal file
15
packages/backend/migration/1710416761960-single-sign-on.js
Normal file
@ -0,0 +1,15 @@
|
||||
export class SingleSignOn1710416761960 {
|
||||
name = 'SingleSignOn1710416761960'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."sso_service_provider_type_enum" AS ENUM('saml', 'jwt')`);
|
||||
await queryRunner.query(`CREATE TABLE "sso_service_provider" ("id" character varying(36) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(256), "type" "public"."sso_service_provider_type_enum" NOT NULL, "issuer" character varying(512) NOT NULL, "audience" character varying(512) array NOT NULL DEFAULT '{}', "acsUrl" character varying(512) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096), "signatureAlgorithm" character varying(100) NOT NULL, "cipherAlgorithm" character varying(100), "wantAuthnRequestsSigned" boolean NOT NULL DEFAULT false, "wantAssertionsSigned" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_0e5fff64534026e48e1c248991a" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_86eee7fa4ae68e4a558dc50961" ON "sso_service_provider" ("createdAt") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_86eee7fa4ae68e4a558dc50961"`);
|
||||
await queryRunner.query(`DROP TABLE "sso_service_provider"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."sso_service_provider_type_enum"`);
|
||||
}
|
||||
}
|
@ -65,16 +65,18 @@
|
||||
"utf-8-validate": "6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.525.0",
|
||||
"@aws-sdk/lib-storage": "3.525.1",
|
||||
"@bull-board/api": "5.14.2",
|
||||
"@bull-board/fastify": "5.14.2",
|
||||
"@bull-board/ui": "5.14.2",
|
||||
"@authenio/samlify-node-xmllint": "2.0.0",
|
||||
"@aws-sdk/client-s3": "3.533.0",
|
||||
"@aws-sdk/lib-storage": "3.533.0",
|
||||
"@bull-board/api": "5.15.1",
|
||||
"@bull-board/fastify": "5.15.1",
|
||||
"@bull-board/ui": "5.15.1",
|
||||
"@discordapp/twemoji": "15.0.2",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
"@fastify/cors": "9.0.1",
|
||||
"@fastify/express": "2.3.0",
|
||||
"@fastify/formbody": "7.4.0",
|
||||
"@fastify/http-proxy": "9.4.0",
|
||||
"@fastify/multipart": "8.1.0",
|
||||
"@fastify/static": "7.0.1",
|
||||
@ -87,7 +89,7 @@
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "9.0.3",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.4.1",
|
||||
"@smithy/node-http-handler": "2.4.3",
|
||||
"@swc/cli": "0.1.65",
|
||||
"@swc/core": "1.3.107",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
@ -107,15 +109,16 @@
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "3.3.1",
|
||||
"date-fns": "3.4.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.26.2",
|
||||
"fastify-http-errors-enhanced": "5.0.3",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.2.0",
|
||||
"got": "14.2.1",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
@ -124,12 +127,13 @@
|
||||
"ip-cidr": "3.1.0",
|
||||
"ipaddr.js": "2.1.0",
|
||||
"is-svg": "5.0.0",
|
||||
"jose": "5.2.3",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "23.2.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
"meilisearch": "0.37.0",
|
||||
"meilisearch": "0.38.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
@ -139,8 +143,8 @@
|
||||
"nanoid": "5.0.6",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.11",
|
||||
"nsfwjs": "3.0.0",
|
||||
"nodemailer": "6.9.12",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
@ -165,13 +169,14 @@
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"samlify": "2.8.11",
|
||||
"sanitize-html": "2.12.1",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.33.2",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.0",
|
||||
"systeminformation": "5.22.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.8",
|
||||
@ -182,7 +187,8 @@
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.16.0",
|
||||
"xev": "3.0.2"
|
||||
"xev": "3.0.2",
|
||||
"xmlbuilder": "15.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
@ -203,13 +209,13 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.6",
|
||||
"@types/jsonld": "1.5.13",
|
||||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/jsrsasign": "10.5.13",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.11.25",
|
||||
"@types/node": "20.11.27",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.3",
|
||||
"@types/oauth2orize": "1.11.4",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.2",
|
||||
"@types/pug": "2.0.10",
|
||||
@ -227,8 +233,8 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"aws-sdk-client-mock": "3.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.57.0",
|
||||
|
1
packages/backend/src/@types/samlify-xsd-schema-validator.d.ts
vendored
Normal file
1
packages/backend/src/@types/samlify-xsd-schema-validator.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '@authenio/samlify-xsd-schema-validator';
|
@ -55,6 +55,7 @@ export const DI = {
|
||||
authSessionsRepository: Symbol('authSessionsRepository'),
|
||||
accessTokensRepository: Symbol('accessTokensRepository'),
|
||||
signinsRepository: Symbol('signinsRepository'),
|
||||
singleSignOnServiceProviderRepository: Symbol('singleSignOnServiceProviderRepository'),
|
||||
pagesRepository: Symbol('pagesRepository'),
|
||||
pageLikesRepository: Symbol('pageLikesRepository'),
|
||||
galleryPostsRepository: Symbol('galleryPostsRepository'),
|
||||
|
@ -36,6 +36,10 @@ export default class Logger {
|
||||
|
||||
this.logger = pino({
|
||||
name: this.domain,
|
||||
serializers: {
|
||||
...pino.stdSerializers,
|
||||
err: pino.stdSerializers.errWithCause,
|
||||
},
|
||||
level: envOption.verbose ? 'debug' : 'info',
|
||||
depthLimit: 8,
|
||||
edgeLimit: 128,
|
||||
@ -63,17 +67,19 @@ export default class Logger {
|
||||
|
||||
@bindThis
|
||||
public error(x: string | Error, context?: Record<string, any> | null, important = false): void { // 実行を継続できない状況で使う
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context === null) context = undefined;
|
||||
if (context?.error) context.error = pino.stdSerializers.errWithCause(context.error);
|
||||
|
||||
if (x instanceof Error) {
|
||||
context = context ?? {};
|
||||
context.error = x;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context?.error === undefined) context = { ...context, error: pino.stdSerializers.errWithCause(x) };
|
||||
|
||||
if (important) this.logger.fatal({ context, important }, x.toString());
|
||||
else this.logger.error({ context, important }, x.toString());
|
||||
} else if (typeof x === 'object') {
|
||||
context = context ?? {};
|
||||
context.error = context.error ?? x;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context?.error === undefined) context = { ...context, error: pino.stdSerializers.errWithCause(x) };
|
||||
|
||||
if (important) this.logger.fatal({ context, important }, `${(x as any).message ?? (x as any).name ?? x}`);
|
||||
else this.logger.error({ context, important }, `${(x as any).message ?? (x as any).name ?? x}`);
|
||||
@ -85,16 +91,18 @@ export default class Logger {
|
||||
|
||||
@bindThis
|
||||
public warn(x: string | Error, context?: Record<string, any> | null, important = false): void { // 実行を継続できるが改善すべき状況で使う
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context === null) context = undefined;
|
||||
if (context?.error) context.error = pino.stdSerializers.errWithCause(context.error);
|
||||
|
||||
if (x instanceof Error) {
|
||||
context = context ?? {};
|
||||
context.error = x;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context?.error === undefined) context = { ...context, error: pino.stdSerializers.errWithCause(x) };
|
||||
|
||||
this.logger.warn({ context, important }, x.toString());
|
||||
} else if (typeof x === 'object') {
|
||||
context = context ?? {};
|
||||
context.error = context.error ?? x;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (context?.error === undefined) context = { ...context, error: pino.stdSerializers.errWithCause(x) };
|
||||
|
||||
this.logger.warn({ context, important }, `${(x as any).message ?? (x as any).name ?? x}`);
|
||||
} else {
|
||||
|
@ -58,6 +58,7 @@ import {
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
MiSignin,
|
||||
MiSingleSignOnServiceProvider,
|
||||
MiSwSubscription,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
@ -325,6 +326,12 @@ const $signinsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $singleSignOnServiceProviderRepository: Provider = {
|
||||
provide: DI.singleSignOnServiceProviderRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSingleSignOnServiceProvider),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $pagesRepository: Provider = {
|
||||
provide: DI.pagesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiPage),
|
||||
@ -538,6 +545,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$authSessionsRepository,
|
||||
$accessTokensRepository,
|
||||
$signinsRepository,
|
||||
$singleSignOnServiceProviderRepository,
|
||||
$pagesRepository,
|
||||
$pageLikesRepository,
|
||||
$galleryPostsRepository,
|
||||
@ -609,6 +617,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$authSessionsRepository,
|
||||
$accessTokensRepository,
|
||||
$signinsRepository,
|
||||
$singleSignOnServiceProviderRepository,
|
||||
$pagesRepository,
|
||||
$pageLikesRepository,
|
||||
$galleryPostsRepository,
|
||||
|
76
packages/backend/src/models/SingleSignOnServiceProvider.ts
Normal file
76
packages/backend/src/models/SingleSignOnServiceProvider.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Column, Index } from 'typeorm';
|
||||
|
||||
@Entity('sso_service_provider')
|
||||
export class MiSingleSignOnServiceProvider {
|
||||
@PrimaryColumn('varchar', {
|
||||
length: 36,
|
||||
})
|
||||
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('enum', {
|
||||
enum: ['saml', 'jwt'],
|
||||
nullable: false,
|
||||
})
|
||||
public type: 'saml' | 'jwt';
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public issuer: string;
|
||||
|
||||
@Column('varchar', {
|
||||
array: true, length: 512, default: '{}',
|
||||
})
|
||||
public audience: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public acsUrl: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 4096,
|
||||
})
|
||||
public publicKey: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 4096, nullable: true,
|
||||
})
|
||||
public privateKey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 100,
|
||||
})
|
||||
public signatureAlgorithm: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 100, nullable: true,
|
||||
})
|
||||
public cipherAlgorithm: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public wantAuthnRequestsSigned: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public wantAssertionsSigned: boolean;
|
||||
}
|
@ -49,6 +49,7 @@ import { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
||||
import { MiRegistryItem } from '@/models/RegistryItem.js';
|
||||
import { MiRelay } from '@/models/Relay.js';
|
||||
import { MiSignin } from '@/models/Signin.js';
|
||||
import { MiSingleSignOnServiceProvider } from '@/models/SingleSignOnServiceProvider.js';
|
||||
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
@ -121,6 +122,7 @@ export {
|
||||
MiRegistryItem,
|
||||
MiRelay,
|
||||
MiSignin,
|
||||
MiSingleSignOnServiceProvider,
|
||||
MiSwSubscription,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
@ -192,6 +194,7 @@ export type RegistrationTicketsRepository = Repository<MiRegistrationTicket>;
|
||||
export type RegistryItemsRepository = Repository<MiRegistryItem>;
|
||||
export type RelaysRepository = Repository<MiRelay>;
|
||||
export type SigninsRepository = Repository<MiSignin>;
|
||||
export type SingleSignOnServiceProviderRepository = Repository<MiSingleSignOnServiceProvider>;
|
||||
export type SwSubscriptionsRepository = Repository<MiSwSubscription>;
|
||||
export type UsedUsernamesRepository = Repository<MiUsedUsername>;
|
||||
export type UsersRepository = Repository<MiUser>;
|
||||
|
@ -59,6 +59,7 @@ import { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
||||
import { MiRegistryItem } from '@/models/RegistryItem.js';
|
||||
import { MiRelay } from '@/models/Relay.js';
|
||||
import { MiSignin } from '@/models/Signin.js';
|
||||
import { MiSingleSignOnServiceProvider } from '@/models/SingleSignOnServiceProvider.js';
|
||||
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
@ -178,6 +179,7 @@ export const entities = [
|
||||
MiAbuseUserReport,
|
||||
MiRegistrationTicket,
|
||||
MiSignin,
|
||||
MiSingleSignOnServiceProvider,
|
||||
MiModerationLog,
|
||||
MiClip,
|
||||
MiClipNote,
|
||||
|
@ -28,6 +28,8 @@ import { FeedService } from './web/FeedService.js';
|
||||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { JWTIdentifyProviderService } from './sso/JWTIdentifyProviderService.js';
|
||||
import { SAMLIdentifyProviderService } from './sso/SAMLIdentifyProviderService.js';
|
||||
import { MainChannelService } from './api/stream/channels/main.js';
|
||||
import { AdminChannelService } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannelService } from './api/stream/channels/antenna.js';
|
||||
@ -89,6 +91,8 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
|
||||
UserListChannelService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
JWTIdentifyProviderService,
|
||||
SAMLIdentifyProviderService,
|
||||
],
|
||||
exports: [
|
||||
ServerService,
|
||||
|
@ -33,6 +33,8 @@ import { FileServerService } from './FileServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { JWTIdentifyProviderService } from './sso/JWTIdentifyProviderService.js';
|
||||
import { SAMLIdentifyProviderService } from './sso/SAMLIdentifyProviderService.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@ -67,6 +69,8 @@ export class ServerService implements OnApplicationShutdown {
|
||||
private globalEventService: GlobalEventService,
|
||||
private loggerService: LoggerService,
|
||||
private oauth2ProviderService: OAuth2ProviderService,
|
||||
private jwtIdentifyProviderService: JWTIdentifyProviderService,
|
||||
private samlIdentifyProviderService: SAMLIdentifyProviderService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
||||
}
|
||||
@ -117,6 +121,9 @@ export class ServerService implements OnApplicationShutdown {
|
||||
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
|
||||
fastify.register(this.oauth2ProviderService.createApiServer, { prefix: '/oauth/api' });
|
||||
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
|
||||
fastify.register(this.samlIdentifyProviderService.createServer, { prefix: '/sso/saml' });
|
||||
fastify.register(this.jwtIdentifyProviderService.createServer, { prefix: '/sso/jwt' });
|
||||
fastify.register(this.jwtIdentifyProviderService.createApiServer, { prefix: '/sso/jwt/api' });
|
||||
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
@ -90,6 +90,10 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
|
||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
||||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||
import * as ep___admin_sso_create from './endpoints/admin/sso/create.js';
|
||||
import * as ep___admin_sso_delete from './endpoints/admin/sso/delete.js';
|
||||
import * as ep___admin_sso_list from './endpoints/admin/sso/list.js';
|
||||
import * as ep___admin_sso_update from './endpoints/admin/sso/update.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
@ -472,6 +476,10 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla
|
||||
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
|
||||
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
|
||||
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
|
||||
const $admin_sso_create: Provider = { provide: 'ep:admin/sso/create', useClass: ep___admin_sso_create.default };
|
||||
const $admin_sso_delete: Provider = { provide: 'ep:admin/sso/delete', useClass: ep___admin_sso_delete.default };
|
||||
const $admin_sso_list: Provider = { provide: 'ep:admin/sso/list', useClass: ep___admin_sso_list.default };
|
||||
const $admin_sso_update: Provider = { provide: 'ep:admin/sso/update', useClass: ep___admin_sso_update.default };
|
||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
||||
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
|
||||
@ -858,6 +866,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_roles_unassign,
|
||||
$admin_roles_updateDefaultPolicies,
|
||||
$admin_roles_users,
|
||||
$admin_sso_create,
|
||||
$admin_sso_delete,
|
||||
$admin_sso_list,
|
||||
$admin_sso_update,
|
||||
$announcements,
|
||||
$antennas_create,
|
||||
$antennas_delete,
|
||||
@ -1238,6 +1250,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_roles_unassign,
|
||||
$admin_roles_updateDefaultPolicies,
|
||||
$admin_roles_users,
|
||||
$admin_sso_create,
|
||||
$admin_sso_delete,
|
||||
$admin_sso_list,
|
||||
$admin_sso_update,
|
||||
$announcements,
|
||||
$antennas_create,
|
||||
$antennas_delete,
|
||||
|
@ -90,6 +90,10 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
|
||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
||||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||
import * as ep___admin_sso_create from './endpoints/admin/sso/create.js';
|
||||
import * as ep___admin_sso_delete from './endpoints/admin/sso/delete.js';
|
||||
import * as ep___admin_sso_list from './endpoints/admin/sso/list.js';
|
||||
import * as ep___admin_sso_update from './endpoints/admin/sso/update.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
@ -470,6 +474,10 @@ const eps = [
|
||||
['admin/roles/unassign', ep___admin_roles_unassign],
|
||||
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
||||
['admin/roles/users', ep___admin_roles_users],
|
||||
['admin/sso/create', ep___admin_sso_create],
|
||||
['admin/sso/delete', ep___admin_sso_delete],
|
||||
['admin/sso/list', ep___admin_sso_list],
|
||||
['admin/sso/update', ep___admin_sso_update],
|
||||
['announcements', ep___announcements],
|
||||
['antennas/create', ep___antennas_create],
|
||||
['antennas/delete', ep___antennas_delete],
|
||||
|
@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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';
|
||||
@ -70,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const indieAuthClient = await this.indieAuthClientsRepository.insert({
|
||||
id: ps.id,
|
||||
createdAt: new Date(),
|
||||
name: ps.name,
|
||||
name: ps.name ? ps.name : null,
|
||||
redirectUris: ps.redirectUris,
|
||||
}).then(r => this.indieAuthClientsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
|
||||
|
||||
|
@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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';
|
||||
|
@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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';
|
||||
|
@ -1,8 +1,3 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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';
|
||||
@ -53,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (client == null) throw new ApiError(meta.errors.noSuchIndieAuthClient);
|
||||
|
||||
await this.indieAuthClientsRepository.update(client.id, {
|
||||
name: ps.name,
|
||||
name: ps.name !== '' ? ps.name : null,
|
||||
redirectUris: ps.redirectUris,
|
||||
});
|
||||
|
||||
|
159
packages/backend/src/server/api/endpoints/admin/sso/create.ts
Normal file
159
packages/backend/src/server/api/endpoints/admin/sso/create.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as jose from 'jose';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
invalidParamSamlUseCertificate: {
|
||||
message: 'SAML service provider must use certificate.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: 'bb97e559-f23c-4d6a-9e4e-eb5db1f467f9',
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['saml', 'jwt'],
|
||||
},
|
||||
issuer: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
audience: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { type: 'string', nullable: false },
|
||||
},
|
||||
acsUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
publicKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
signatureAlgorithm: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cipherAlgorithm: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
wantAuthnRequestsSigned: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
wantAssertionsSigned: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', nullable: true },
|
||||
type: { type: 'string', enum: ['saml', 'jwt'], nullable: false },
|
||||
issuer: { type: 'string', nullable: false },
|
||||
audience: { type: 'array', items: { type: 'string', nullable: false }, default: [] },
|
||||
acsUrl: { type: 'string', nullable: false },
|
||||
signatureAlgorithm: { type: 'string', nullable: false },
|
||||
cipherAlgorithm: { type: 'string', nullable: true },
|
||||
wantAuthnRequestsSigned: { type: 'boolean', nullable: false, default: false },
|
||||
wantAssertionsSigned: { type: 'boolean', nullable: false, default: true },
|
||||
useCertificate: { type: 'boolean', nullable: false, default: true },
|
||||
secret: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['type', 'issuer', 'acsUrl', 'signatureAlgorithm', 'useCertificate'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.type === 'saml' && ps.useCertificate === false) {
|
||||
throw new ApiError(meta.errors.invalidParamSamlUseCertificate);
|
||||
}
|
||||
|
||||
const { publicKey, privateKey } = ps.useCertificate
|
||||
? await jose.generateKeyPair(ps.signatureAlgorithm).then(async keypair => ({
|
||||
publicKey: JSON.stringify(await jose.exportJWK(keypair.publicKey)),
|
||||
privateKey: JSON.stringify(await jose.exportJWK(keypair.privateKey)),
|
||||
}))
|
||||
: { publicKey: ps.secret ?? randomUUID(), privateKey: null };
|
||||
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.insert({
|
||||
id: randomUUID(),
|
||||
createdAt: new Date(),
|
||||
name: ps.name ? ps.name : null,
|
||||
type: ps.type,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey,
|
||||
signatureAlgorithm: ps.signatureAlgorithm,
|
||||
cipherAlgorithm: ps.cipherAlgorithm ? ps.cipherAlgorithm : null,
|
||||
wantAuthnRequestsSigned: ps.wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned: ps.wantAssertionsSigned,
|
||||
}).then(r => this.singleSignOnServiceProviderRepository.findOneByOrFail({ id: r.identifiers[0].id }));
|
||||
|
||||
this.moderationLogService.log(me, 'createSSOServiceProvider', {
|
||||
serviceId: ssoServiceProvider.id,
|
||||
service: ssoServiceProvider,
|
||||
});
|
||||
|
||||
return {
|
||||
id: ssoServiceProvider.id,
|
||||
createdAt: ssoServiceProvider.createdAt.toISOString(),
|
||||
name: ssoServiceProvider.name,
|
||||
type: ssoServiceProvider.type,
|
||||
issuer: ssoServiceProvider.issuer,
|
||||
audience: ssoServiceProvider.audience,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
publicKey: ssoServiceProvider.publicKey,
|
||||
signatureAlgorithm: ssoServiceProvider.signatureAlgorithm,
|
||||
cipherAlgorithm: ssoServiceProvider.cipherAlgorithm,
|
||||
wantAuthnRequestsSigned: ssoServiceProvider.wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned: ssoServiceProvider.wantAssertionsSigned,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
noSuchSingleSignOnServiceProvider: {
|
||||
message: 'No such SSO Service Provider',
|
||||
code: 'NO_SUCH_SSO_SP',
|
||||
id: 'ece541d3-6c41-4fc3-a514-fa762b96704a',
|
||||
},
|
||||
},
|
||||
} 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.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const service = await this.singleSignOnServiceProviderRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (service == null) throw new ApiError(meta.errors.noSuchSingleSignOnServiceProvider);
|
||||
|
||||
await this.singleSignOnServiceProviderRepository.delete(service.id);
|
||||
|
||||
this.moderationLogService.log(me, 'deleteSSOServiceProvider', {
|
||||
serviceId: service.id,
|
||||
service: service,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
111
packages/backend/src/server/api/endpoints/admin/sso/list.ts
Normal file
111
packages/backend/src/server/api/endpoints/admin/sso/list.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:sso',
|
||||
|
||||
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,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['saml', 'jwt'],
|
||||
},
|
||||
issuer: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
audience: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { type: 'string', nullable: false },
|
||||
},
|
||||
acsUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
publicKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
signatureAlgorithm: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cipherAlgorithm: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
wantAuthnRequestsSigned: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
wantAssertionsSigned: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} 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.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.singleSignOnServiceProviderRepository.createQueryBuilder('service');
|
||||
const services = await query.offset(ps.offset).limit(ps.limit).getMany();
|
||||
|
||||
return services.map(service => ({
|
||||
id: service.id,
|
||||
createdAt: service.createdAt.toISOString(),
|
||||
name: service.name,
|
||||
type: service.type,
|
||||
issuer: service.issuer,
|
||||
audience: service.audience,
|
||||
acsUrl: service.acsUrl,
|
||||
publicKey: service.publicKey,
|
||||
signatureAlgorithm: service.signatureAlgorithm,
|
||||
cipherAlgorithm: service.cipherAlgorithm,
|
||||
wantAuthnRequestsSigned: service.wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned: service.wantAssertionsSigned,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import * as jose from 'jose';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { SingleSignOnServiceProviderRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
noSuchSingleSignOnServiceProvider: {
|
||||
message: 'No such SSO Service Provider',
|
||||
code: 'NO_SUCH_SSO_SP',
|
||||
id: '2f481db0-23f5-4380-8cb8-704169ffb25b',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
issuer: { type: 'string' },
|
||||
audience: { type: 'array', items: { type: 'string', nullable: false } },
|
||||
acsUrl: { type: 'string' },
|
||||
signatureAlgorithm: { type: 'string' },
|
||||
cipherAlgorithm: { type: 'string' },
|
||||
wantAuthnRequestsSigned: { type: 'boolean' },
|
||||
wantAssertionsSigned: { type: 'boolean' },
|
||||
regenerateCertificate: { type: 'boolean' },
|
||||
secret: { 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.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const service = await this.singleSignOnServiceProviderRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (service == null) throw new ApiError(meta.errors.noSuchSingleSignOnServiceProvider);
|
||||
|
||||
const alg = ps.signatureAlgorithm ? ps.signatureAlgorithm : service.signatureAlgorithm;
|
||||
const { publicKey, privateKey } = ps.regenerateCertificate
|
||||
? await jose.generateKeyPair(alg).then(async keypair => ({
|
||||
publicKey: JSON.stringify(await jose.exportJWK(keypair.publicKey)),
|
||||
privateKey: JSON.stringify(await jose.exportJWK(keypair.privateKey)),
|
||||
}))
|
||||
: { publicKey: ps.secret ?? undefined, privateKey: undefined };
|
||||
|
||||
await this.singleSignOnServiceProviderRepository.update(service.id, {
|
||||
name: ps.name !== '' ? ps.name : null,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey,
|
||||
signatureAlgorithm: ps.signatureAlgorithm,
|
||||
cipherAlgorithm: ps.cipherAlgorithm !== '' ? ps.cipherAlgorithm : null,
|
||||
wantAuthnRequestsSigned: ps.wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned: ps.wantAssertionsSigned,
|
||||
});
|
||||
|
||||
const updatedService = await this.singleSignOnServiceProviderRepository.findOneByOrFail({ id: service.id });
|
||||
|
||||
this.moderationLogService.log(me, 'updateSSOServiceProvider', {
|
||||
serviceId: service.id,
|
||||
before: service,
|
||||
after: updatedService,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ import type {
|
||||
AccessTokensRepository,
|
||||
IndieAuthClientsRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
@ -474,7 +474,7 @@ export class OAuth2ProviderService {
|
||||
fastify.use('/decision', this.#server.decision((req, done) => {
|
||||
const { body } = req as OAuth2DecisionRequest;
|
||||
this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`);
|
||||
req.user = body.login_token;
|
||||
if (!body.cancel) req.user = body.login_token;
|
||||
done(null, undefined);
|
||||
}));
|
||||
fastify.use('/decision', this.#server.errorHandler());
|
||||
@ -508,7 +508,7 @@ export class OAuth2ProviderService {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessToken = await this.accessTokensRepository.findOneBy({ token });
|
||||
const accessToken = await this.accessTokensRepository.findOne({ where: { token }, relations: ['user'] });
|
||||
if (!accessToken) {
|
||||
reply.code(401);
|
||||
return;
|
||||
@ -525,7 +525,8 @@ export class OAuth2ProviderService {
|
||||
picture: accessToken.user?.avatarUrl,
|
||||
email: user?.email,
|
||||
email_verified: user?.emailVerified,
|
||||
updated_at: (accessToken.lastUsedAt?.getTime() ?? 0) / 1000,
|
||||
mfa_enabled: user?.twoFactorEnabled,
|
||||
updated_at: (accessToken.user?.updatedAt?.getTime() ?? accessToken.user?.createdAt.getTime() ?? 0) / 1000,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -543,7 +544,7 @@ export class OAuth2ProviderService {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessToken = await this.accessTokensRepository.findOneBy({ token });
|
||||
const accessToken = await this.accessTokensRepository.findOne({ where: { token }, relations: ['user'] });
|
||||
reply.code(200);
|
||||
|
||||
if (!accessToken) return { active: false };
|
||||
|
375
packages/backend/src/server/sso/JWTIdentifyProviderService.ts
Normal file
375
packages/backend/src/server/sso/JWTIdentifyProviderService.ts
Normal file
@ -0,0 +1,375 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import pug from 'pug';
|
||||
import fastifyView from '@fastify/view';
|
||||
import fastifyCors from '@fastify/cors';
|
||||
import fastifyFormbody from '@fastify/formbody';
|
||||
import fastifyHttpErrorsEnhanced from 'fastify-http-errors-enhanced';
|
||||
import * as jose from 'jose';
|
||||
import { JWTPayload } from 'jose';
|
||||
import Logger from '@/logger.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type {
|
||||
SingleSignOnServiceProviderRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class JWTIdentifyProviderService {
|
||||
#logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private cacheService: CacheService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.#logger = this.loggerService.getLogger('sso:jwt');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createServer(fastify: FastifyInstance): Promise<void> {
|
||||
fastify.register(fastifyHttpErrorsEnhanced, { preHandler: (error: Error): Error => { this.#logger.error(error); return error; } });
|
||||
fastify.register(fastifyFormbody);
|
||||
fastify.register(fastifyCors);
|
||||
fastify.register(fastifyView, {
|
||||
root: fileURLToPath(new URL('../web/views', import.meta.url)),
|
||||
engine: { pug },
|
||||
defaultContext: {
|
||||
version: this.config.version,
|
||||
config: this.config,
|
||||
},
|
||||
});
|
||||
|
||||
fastify.all<{
|
||||
Params: { serviceId: string };
|
||||
Querystring?: { return_to?: string };
|
||||
Body?: { return_to?: string };
|
||||
}>('/:serviceId', async (request, reply) => {
|
||||
const serviceId = request.params.serviceId;
|
||||
const returnTo = request.query?.return_to ?? request.body?.return_to;
|
||||
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: 'c6aafae6-e8b9-420c-a87a-6ac08402165b',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const transactionId = randomUUID();
|
||||
await this.redisClient.set(
|
||||
`sso:jwt:transaction:${transactionId}`,
|
||||
JSON.stringify({
|
||||
serviceId: serviceId,
|
||||
returnTo: returnTo,
|
||||
}),
|
||||
'EX',
|
||||
60 * 5,
|
||||
);
|
||||
|
||||
this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return await reply.view('sso', {
|
||||
transactionId: transactionId,
|
||||
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
|
||||
kind: 'jwt',
|
||||
});
|
||||
});
|
||||
|
||||
fastify.post<{
|
||||
Body: { transaction_id: string; login_token: string; cancel?: string };
|
||||
}>('/authorize', async (request, reply) => {
|
||||
const transactionId = request.body.transaction_id;
|
||||
const token = request.body.login_token;
|
||||
const cancel = !!request.body.cancel;
|
||||
|
||||
if (cancel) {
|
||||
reply.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = await this.redisClient.get(`sso:jwt:transaction:${transactionId}`);
|
||||
if (!transaction) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid transaction id',
|
||||
code: 'INVALID_TRANSACTION_ID',
|
||||
id: '91fa6511-0b33-47d6-bd01-b420d80fcd6a',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { serviceId, returnTo } = JSON.parse(transaction);
|
||||
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: 'c038610c-4c11-40ce-9371-131d5720f511',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
reply.status(401).send({
|
||||
error: {
|
||||
message: 'No login token',
|
||||
code: 'NO_LOGIN_TOKEN',
|
||||
id: '399e756c-35cd-459c-a7ba-8cc12eb39eef',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(
|
||||
token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>,
|
||||
);
|
||||
if (!user) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid login token',
|
||||
code: 'INVALID_LOGIN_TOKEN',
|
||||
id: '3b92ee31-9215-447a-805f-df8f15ffb8b2',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
const isAdministrator = await this.roleService.isAdministrator(user);
|
||||
const isModerator = await this.roleService.isModerator(user);
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
const payload: JWTPayload = {
|
||||
name: user.name,
|
||||
preferred_username: user.username,
|
||||
profile: `${this.config.url}/@${user.username}`,
|
||||
picture: user.avatarUrl,
|
||||
email: profile.email,
|
||||
email_verified: profile.emailVerified,
|
||||
mfa_enabled: profile.twoFactorEnabled,
|
||||
updated_at: (user.updatedAt?.getTime() ?? user.createdAt.getTime()) / 1000,
|
||||
admin: isAdministrator,
|
||||
moderator: isModerator,
|
||||
roles: roles.filter(r => r.isPublic).map(r => r.id),
|
||||
};
|
||||
|
||||
try {
|
||||
if (ssoServiceProvider.cipherAlgorithm) {
|
||||
const key = ssoServiceProvider.publicKey.startsWith('{')
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.publicKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const jwt = await new jose.EncryptJWT(payload)
|
||||
.setProtectedHeader({
|
||||
alg: ssoServiceProvider.signatureAlgorithm,
|
||||
enc: ssoServiceProvider.cipherAlgorithm,
|
||||
})
|
||||
.setIssuer(ssoServiceProvider.issuer)
|
||||
.setAudience(ssoServiceProvider.audience)
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('10m')
|
||||
.setJti(randomUUID())
|
||||
.setSubject(user.id)
|
||||
.encrypt(key);
|
||||
|
||||
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
returnTo,
|
||||
});
|
||||
|
||||
if (returnTo) {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}&return_to=${returnTo}`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const key = ssoServiceProvider.privateKey
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.privateKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const jwt = await new jose.SignJWT(payload)
|
||||
.setProtectedHeader({ alg: ssoServiceProvider.signatureAlgorithm })
|
||||
.setIssuer(ssoServiceProvider.issuer)
|
||||
.setAudience(ssoServiceProvider.audience)
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('10m')
|
||||
.setJti(randomUUID())
|
||||
.setSubject(user.id)
|
||||
.sign(key);
|
||||
|
||||
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
returnTo,
|
||||
});
|
||||
|
||||
if (returnTo) {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}&return_to=${returnTo}`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.#logger.error('Failed to create JWT', { error: err });
|
||||
const traceableError = err as Error & { code?: string };
|
||||
|
||||
if (traceableError.code) {
|
||||
reply.status(500).send({
|
||||
error: {
|
||||
message: traceableError.message,
|
||||
code: traceableError.code,
|
||||
id: 'a436fa15-20ca-4269-ac4d-ee162fe1f3b0',
|
||||
kind: 'server',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(500).send({
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
id: 'fe1c597c-a515-46a1-860b-bd316b11aff9',
|
||||
kind: 'server',
|
||||
},
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
await this.redisClient.del(`sso:jwt:transaction:${transactionId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createApiServer(fastify: FastifyInstance): Promise<void> {
|
||||
fastify.register(fastifyHttpErrorsEnhanced, { preHandler: (error: Error): Error => { this.#logger.error(error); return error; } });
|
||||
fastify.register(fastifyFormbody);
|
||||
fastify.register(fastifyCors);
|
||||
|
||||
fastify.post<{
|
||||
Params: { serviceId: string };
|
||||
Body: { jwt: string };
|
||||
}>('/verify/:serviceId', async (request, reply) => {
|
||||
const serviceId = request.params.serviceId;
|
||||
const jwt = request.body.jwt;
|
||||
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: '077e0930-88c1-4f25-bd4e-4da8e34f735b',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (ssoServiceProvider.cipherAlgorithm) {
|
||||
const key = ssoServiceProvider.privateKey
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.privateKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const { payload } = await jose.jwtDecrypt(jwt, key, {
|
||||
issuer: ssoServiceProvider.issuer,
|
||||
audience: ssoServiceProvider.audience,
|
||||
});
|
||||
|
||||
reply.status(200).send({ payload });
|
||||
return;
|
||||
} else {
|
||||
const key = ssoServiceProvider.publicKey.startsWith('{')
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.publicKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const { payload } = await jose.jwtVerify(jwt, key, {
|
||||
issuer: ssoServiceProvider.issuer,
|
||||
audience: ssoServiceProvider.audience,
|
||||
});
|
||||
|
||||
reply.status(200).send({ payload });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
this.#logger.error('Failed to verify JWT', { error: err });
|
||||
const traceableError = err as Error & { code?: string };
|
||||
|
||||
if (traceableError.code) {
|
||||
reply.status(400).send({
|
||||
error: {
|
||||
message: traceableError.message,
|
||||
code: traceableError.code,
|
||||
id: '843421cf-3ab3-4b1f-ade4-5d5ce1efb6be',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(400).send({
|
||||
error: {
|
||||
message: 'Invalid JWT',
|
||||
code: 'INVALID_JWT',
|
||||
id: '39075dbb-03eb-485f-8ee1-f16b625bcc4d',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
654
packages/backend/src/server/sso/SAMLIdentifyProviderService.ts
Normal file
654
packages/backend/src/server/sso/SAMLIdentifyProviderService.ts
Normal file
@ -0,0 +1,654 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import * as jose from 'jose';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as saml from 'samlify';
|
||||
import * as validator from '@authenio/samlify-node-xmllint';
|
||||
import fastifyView from '@fastify/view';
|
||||
import fastifyCors from '@fastify/cors';
|
||||
import fastifyFormbody from '@fastify/formbody';
|
||||
import fastifyHttpErrorsEnhanced from 'fastify-http-errors-enhanced';
|
||||
import pug from 'pug';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import Logger from '@/logger.js';
|
||||
import type {
|
||||
MiSingleSignOnServiceProvider,
|
||||
SingleSignOnServiceProviderRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class SAMLIdentifyProviderService {
|
||||
#logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.singleSignOnServiceProviderRepository)
|
||||
private singleSignOnServiceProviderRepository: SingleSignOnServiceProviderRepository,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private cacheService: CacheService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.#logger = this.loggerService.getLogger('sso:saml');
|
||||
saml.setSchemaValidator(validator);
|
||||
}
|
||||
|
||||
public async createIdPMetadataXml(
|
||||
provider: MiSingleSignOnServiceProvider,
|
||||
): Promise<string> {
|
||||
const today = new Date();
|
||||
const publicKey = await jose.importJWK(JSON.parse(provider.publicKey)).then((r) => jose.exportSPKI(r as jose.KeyLike));
|
||||
|
||||
const nodes = {
|
||||
'md:EntityDescriptor': {
|
||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'@entityID': provider.issuer,
|
||||
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
|
||||
'md:IDPSSODescriptor': {
|
||||
'@WantAuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
||||
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'md:KeyDescriptor': {
|
||||
'@use': 'signing',
|
||||
'ds:KeyInfo': {
|
||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'ds:X509Data': {
|
||||
'ds:X509Certificate': {
|
||||
'#text': publicKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'md:NameIDFormat': {
|
||||
'#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||
},
|
||||
'md:SingleSignOnService': [
|
||||
{
|
||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
||||
},
|
||||
{
|
||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return xmlbuilder
|
||||
.create(nodes, { encoding: 'UTF-8', standalone: false })
|
||||
.end({ pretty: true });
|
||||
}
|
||||
|
||||
public async createSPMetadataXml(
|
||||
provider: MiSingleSignOnServiceProvider,
|
||||
): Promise<string> {
|
||||
const today = new Date();
|
||||
const publicKey = await jose.importJWK(JSON.parse(provider.publicKey)).then((r) => jose.exportSPKI(r as jose.KeyLike));
|
||||
|
||||
const keyDescriptor: unknown[] = [
|
||||
{
|
||||
'@use': 'signing',
|
||||
'ds:KeyInfo': {
|
||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'ds:X509Data': {
|
||||
'ds:X509Certificate': {
|
||||
'#text': publicKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (provider.cipherAlgorithm) {
|
||||
keyDescriptor.push({
|
||||
'@use': 'encryption',
|
||||
'ds:KeyInfo': {
|
||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'ds:X509Data': {
|
||||
'ds:X509Certificate': {
|
||||
'#text': publicKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
'md:EncryptionMethod': {
|
||||
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const nodes = {
|
||||
'md:EntityDescriptor': {
|
||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'@entityID': provider.issuer,
|
||||
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
|
||||
'md:SPSSODescriptor': {
|
||||
'@AuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
||||
'@WantAssertionsSigned': provider.wantAssertionsSigned,
|
||||
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'md:KeyDescriptor': keyDescriptor,
|
||||
'md:NameIDFormat': {
|
||||
'#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||
},
|
||||
'md:AssertionConsumerService': {
|
||||
'@index': 1,
|
||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
||||
'@Location': provider.acsUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return xmlbuilder
|
||||
.create(nodes, { encoding: 'UTF-8', standalone: false })
|
||||
.end({ pretty: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Alternative to lodash.get
|
||||
* @reference https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get
|
||||
* @param obj
|
||||
* @param path
|
||||
* @param defaultValue
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private get(obj: any, path: string, defaultValue: unknown) {
|
||||
return path
|
||||
.split('.')
|
||||
.reduce((a, c) => (a?.[c] ? a[c] : defaultValue || null), obj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createServer(fastify: FastifyInstance): Promise<void> {
|
||||
fastify.register(fastifyHttpErrorsEnhanced, { preHandler: (error: Error): Error => { this.#logger.error(error); return error; } });
|
||||
fastify.register(fastifyFormbody);
|
||||
fastify.register(fastifyCors);
|
||||
fastify.register(fastifyView, {
|
||||
root: fileURLToPath(new URL('../web/views', import.meta.url)),
|
||||
engine: { pug },
|
||||
defaultContext: {
|
||||
version: this.config.version,
|
||||
config: this.config,
|
||||
},
|
||||
});
|
||||
|
||||
fastify.all<{
|
||||
Params: { serviceId: string };
|
||||
Querystring?: { SAMLRequest?: string; RelayState?: string };
|
||||
Body?: { SAMLRequest?: string; RelayState?: string };
|
||||
}>('/:serviceId', async (request, reply) => {
|
||||
const serviceId = request.params.serviceId;
|
||||
const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
|
||||
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
|
||||
const relayState = request.query?.RelayState ?? request.body?.RelayState;
|
||||
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml', privateKey: Not(IsNull()) });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: 'e2893d7e-df6f-44cf-8717-42234b8ac0ce',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!samlRequest) {
|
||||
reply.status(400).send({
|
||||
error: {
|
||||
message: 'No SAMLRequest',
|
||||
code: 'NO_SAML_REQUEST',
|
||||
id: 'c58bc7e3-f92e-4879-a6a9-7258a13bc491',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const idp = saml.IdentityProvider({
|
||||
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
||||
privateKey: await jose
|
||||
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
||||
.then((r) => jose.exportPKCS8(r as jose.KeyLike)),
|
||||
});
|
||||
|
||||
const sp = saml.ServiceProvider({
|
||||
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
||||
});
|
||||
|
||||
const parsed = await idp.parseLoginRequest(sp, binding, { query: request.query, body: request.body });
|
||||
this.#logger.info('Parsed SAML request', { saml: parsed });
|
||||
|
||||
const transactionId = randomUUID();
|
||||
await this.redisClient.set(
|
||||
`sso:saml:transaction:${transactionId}`,
|
||||
JSON.stringify({
|
||||
serviceId: serviceId,
|
||||
binding: binding,
|
||||
flowResult: parsed,
|
||||
relayState: relayState,
|
||||
}),
|
||||
'EX',
|
||||
60 * 5,
|
||||
);
|
||||
|
||||
this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return await reply.view('sso', {
|
||||
transactionId: transactionId,
|
||||
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
|
||||
kind: 'saml',
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { serviceId: string } }>(
|
||||
'/:serviceId/metadata',
|
||||
async (request, reply) => {
|
||||
const serviceId = request.params.serviceId;
|
||||
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml' });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: '8a6d72e1-3530-4ec0-9d4d-b105fdbb8a2d',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Content-Type', 'application/xml');
|
||||
reply.send(await this.createIdPMetadataXml(ssoServiceProvider));
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post<{
|
||||
Body: { transaction_id: string; login_token: string; cancel?: string };
|
||||
}>('/authorize', async (request, reply) => {
|
||||
const transactionId = request.body.transaction_id;
|
||||
const token = request.body.login_token;
|
||||
const cancel = !!request.body.cancel;
|
||||
|
||||
if (cancel) {
|
||||
reply.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = await this.redisClient.get(`sso:saml:transaction:${transactionId}`);
|
||||
if (!transaction) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid transaction id',
|
||||
code: 'INVALID_TRANSACTION_ID',
|
||||
id: 'cca6ea16-5f04-4d9e-9ef5-8a99bdef3a92',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { serviceId, binding, flowResult, relayState } = JSON.parse(transaction);
|
||||
|
||||
const ssoServiceProvider =
|
||||
await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml' });
|
||||
if (!ssoServiceProvider) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid SSO Service Provider id',
|
||||
code: 'INVALID_SSO_SP_ID',
|
||||
id: 'f644adfe-019a-478c-b5a9-897a2556f2b2',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
reply.status(401).send({
|
||||
error: {
|
||||
message: 'No login token',
|
||||
code: 'NO_LOGIN_TOKEN',
|
||||
id: 'cd96295e-0370-433d-a3de-421de4536b7f',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(
|
||||
token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>,
|
||||
);
|
||||
if (!user) {
|
||||
reply.status(403).send({
|
||||
error: {
|
||||
message: 'Invalid login token',
|
||||
code: 'INVALID_LOGIN_TOKEN',
|
||||
id: 'a002a4ed-0024-460f-8015-cc5e7c6cd0a7',
|
||||
kind: 'client',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
const isAdministrator = await this.roleService.isAdministrator(user);
|
||||
const isModerator = await this.roleService.isModerator(user);
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
try {
|
||||
const idp = saml.IdentityProvider({
|
||||
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
||||
privateKey: await jose
|
||||
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
||||
.then((r) => jose.exportPKCS8(r as jose.KeyLike)),
|
||||
loginResponseTemplate: { context: 'ignored' },
|
||||
});
|
||||
|
||||
const sp = saml.ServiceProvider({
|
||||
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
||||
});
|
||||
|
||||
const samlResponse = await idp.createLoginResponse(
|
||||
sp,
|
||||
flowResult,
|
||||
binding,
|
||||
{},
|
||||
() => {
|
||||
const id = idp.entitySetting.generateID?.() ?? randomUUID();
|
||||
const assertionId = idp.entitySetting.generateID?.() ?? randomUUID();
|
||||
const nowTime = new Date();
|
||||
const fiveMinutesLaterTime = new Date(nowTime.getTime());
|
||||
fiveMinutesLaterTime.setMinutes(fiveMinutesLaterTime.getMinutes() + 5);
|
||||
const now = nowTime.toISOString();
|
||||
const fiveMinutesLater = fiveMinutesLaterTime.toISOString();
|
||||
|
||||
const nodes = {
|
||||
'samlp:Response': {
|
||||
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
||||
'@ID': id,
|
||||
'@Version': '2.0',
|
||||
'@IssueInstant': now,
|
||||
'@Destination': ssoServiceProvider.acsUrl,
|
||||
'@InResponseTo': this.get(flowResult, 'extract.request.id', ''),
|
||||
'saml:Issuer': {
|
||||
'#text': ssoServiceProvider.issuer,
|
||||
},
|
||||
'samlp:Status': {
|
||||
'samlp:StatusCode': {
|
||||
'@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success',
|
||||
},
|
||||
},
|
||||
'saml:Assertion': {
|
||||
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'@xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
|
||||
'@ID': assertionId,
|
||||
'@Version': '2.0',
|
||||
'@IssueInstant': now,
|
||||
'saml:Issuer': {
|
||||
'#text': ssoServiceProvider.issuer,
|
||||
},
|
||||
'saml:Subject': {
|
||||
'saml:NameID': {
|
||||
'@Format':
|
||||
'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||
'#text': user.id,
|
||||
},
|
||||
'saml:SubjectConfirmation': {
|
||||
'@Method': 'urn:oasis:names:tc:SAML:2.0:cm:bearer',
|
||||
'saml:SubjectConfirmationData': {
|
||||
'@InResponseTo': this.get(flowResult, 'extract.request.id', ''),
|
||||
'@NotOnOrAfter': fiveMinutesLater,
|
||||
'@Recipient': ssoServiceProvider.acsUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
'saml:Conditions': {
|
||||
'@NotBefore': now,
|
||||
'@NotOnOrAfter': fiveMinutesLater,
|
||||
'saml:AudienceRestriction': {
|
||||
'saml:Audience': [
|
||||
{ '#text': ssoServiceProvider.issuer },
|
||||
...ssoServiceProvider.audience.map((audience) => ({
|
||||
'#text': audience,
|
||||
})),
|
||||
],
|
||||
},
|
||||
},
|
||||
'saml:AuthnStatement': {
|
||||
'@AuthnInstant': now,
|
||||
'@SessionIndex': assertionId,
|
||||
'@SessionNotOnOrAfter': fiveMinutesLater,
|
||||
'saml:AuthnContext': {
|
||||
'saml:AuthnContextClassRef': {
|
||||
'#text':
|
||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
|
||||
},
|
||||
},
|
||||
},
|
||||
'saml:AttributeStatement': {
|
||||
'saml:Attribute': [
|
||||
{
|
||||
'@Name': 'identityprovider',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': this.config.url,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'uid',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': user.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'displayname',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': user.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'name',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': user.username,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'preferred_username',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': user.username,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'profile',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': `${this.config.url}/@${user.username}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'picture',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': user.avatarUrl,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'mail',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': profile.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'email',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': profile.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'email_verified',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:boolean',
|
||||
'#text': profile.emailVerified,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'mfa_enabled',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:boolean',
|
||||
'#text': profile.twoFactorEnabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'updated_at',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:integer',
|
||||
'#text': (user.updatedAt?.getTime() ?? user.createdAt.getTime()) / 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'admin',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:boolean',
|
||||
'#text': isAdministrator,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'moderator',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': {
|
||||
'@xsi:type': 'xs:boolean',
|
||||
'#text': isModerator,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@Name': 'roles',
|
||||
'@NameFormat':
|
||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||
'saml:AttributeValue': [
|
||||
...roles
|
||||
.filter((r) => r.isPublic)
|
||||
.map((r) => ({
|
||||
'@xsi:type': 'xs:string',
|
||||
'#text': r.id,
|
||||
})),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
context: xmlbuilder
|
||||
.create(nodes, { encoding: 'UTF-8', standalone: false })
|
||||
.end({ pretty: false }),
|
||||
};
|
||||
},
|
||||
undefined,
|
||||
relayState,
|
||||
);
|
||||
|
||||
this.#logger.info(`Rendering SAML response page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
relayState: relayState,
|
||||
});
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return await reply.view('sso-saml-post', {
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
samlResponse: samlResponse,
|
||||
relyState: relayState ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
this.#logger.error('Failed to create SAML response', { error: err });
|
||||
const traceableError = err as Error & { code?: string };
|
||||
|
||||
if (traceableError.code) {
|
||||
reply.status(500).send({
|
||||
error: {
|
||||
message: traceableError.message,
|
||||
code: traceableError.code,
|
||||
id: 'a743ff78-8636-4b69-a54f-e3b395564f79',
|
||||
kind: 'server',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(500).send({
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
id: 'b83b7afd-adfc-4baf-8659-34623d639170',
|
||||
kind: 'server',
|
||||
},
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
await this.redisClient.del(`sso:saml:transaction:${transactionId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
21
packages/backend/src/server/web/views/sso-saml-post.pug
Normal file
21
packages/backend/src/server/web/views/sso-saml-post.pug
Normal file
@ -0,0 +1,21 @@
|
||||
html
|
||||
body
|
||||
noscript: p
|
||||
| JavaScriptを有効にしてください
|
||||
br
|
||||
| Please turn on your JavaScript
|
||||
|
||||
p
|
||||
| Redirecting...
|
||||
|
||||
form(id='sso', method='post', action=action autocomplete='off')
|
||||
input(type='hidden', name='SAMLResponse', value=samlResponse)
|
||||
|
||||
if relayState !== null
|
||||
input(type='hidden', name='RelayState', value=relayState)
|
||||
|
||||
button(type='submit')
|
||||
| click here if you are not redirected.
|
||||
|
||||
script.
|
||||
document.forms[0].submit();
|
6
packages/backend/src/server/web/views/sso.pug
Normal file
6
packages/backend/src/server/web/views/sso.pug
Normal file
@ -0,0 +1,6 @@
|
||||
extends ./base
|
||||
|
||||
block meta
|
||||
meta(name='misskey:sso:transaction-id' content=transactionId)
|
||||
meta(name='misskey:sso:service-name' content=serviceName)
|
||||
meta(name='misskey:sso:kind' content=kind)
|
@ -88,6 +88,9 @@ export const moderationLogTypes = [
|
||||
'createIndieAuthClient',
|
||||
'updateIndieAuthClient',
|
||||
'deleteIndieAuthClient',
|
||||
'createSSOServiceProvider',
|
||||
'updateSSOServiceProvider',
|
||||
'deleteSSOServiceProvider',
|
||||
'createAvatarDecoration',
|
||||
'updateAvatarDecoration',
|
||||
'deleteAvatarDecoration',
|
||||
@ -273,6 +276,19 @@ export type ModerationLogPayloads = {
|
||||
clientId: string;
|
||||
client: any;
|
||||
};
|
||||
createSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
service: any;
|
||||
};
|
||||
updateSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
before: any;
|
||||
after: any;
|
||||
};
|
||||
deleteSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
service: any;
|
||||
};
|
||||
createAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
avatarDecoration: any;
|
||||
|
@ -40,10 +40,10 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "11.0.4",
|
||||
"chromatic": "11.0.8",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "3.3.1",
|
||||
"date-fns": "3.4.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
@ -58,9 +58,9 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.12.1",
|
||||
"rollup": "4.13.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.71.1",
|
||||
"sass": "1.72.0",
|
||||
"shiki": "1.1.7",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
@ -71,72 +71,72 @@
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.4.2",
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.9.0",
|
||||
"vite": "5.1.5",
|
||||
"v-code-diff": "1.10.0",
|
||||
"vite": "5.1.6",
|
||||
"vue": "3.4.15",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@misskey-dev/summaly": "5.0.3",
|
||||
"@storybook/addon-actions": "8.0.0-beta.6",
|
||||
"@storybook/addon-essentials": "8.0.0-beta.6",
|
||||
"@storybook/addon-interactions": "8.0.0-beta.6",
|
||||
"@storybook/addon-links": "8.0.0-beta.6",
|
||||
"@storybook/addon-mdx-gfm": "8.0.0-beta.6",
|
||||
"@storybook/addon-storysource": "8.0.0-beta.6",
|
||||
"@storybook/blocks": "8.0.0-beta.6",
|
||||
"@storybook/components": "8.0.0-beta.6",
|
||||
"@storybook/core-events": "8.0.0-beta.6",
|
||||
"@storybook/manager-api": "8.0.0-beta.6",
|
||||
"@storybook/preview-api": "8.0.0-beta.6",
|
||||
"@storybook/react": "8.0.0-beta.6",
|
||||
"@storybook/react-vite": "8.0.0-beta.6",
|
||||
"@storybook/test": "8.0.0-beta.6",
|
||||
"@storybook/theming": "8.0.0-beta.6",
|
||||
"@storybook/types": "8.0.0-beta.6",
|
||||
"@storybook/vue3": "8.0.0-beta.6",
|
||||
"@storybook/vue3-vite": "8.0.0-beta.6",
|
||||
"@storybook/addon-actions": "8.0.0",
|
||||
"@storybook/addon-essentials": "8.0.0",
|
||||
"@storybook/addon-interactions": "8.0.0",
|
||||
"@storybook/addon-links": "8.0.0",
|
||||
"@storybook/addon-mdx-gfm": "8.0.0",
|
||||
"@storybook/addon-storysource": "8.0.0",
|
||||
"@storybook/blocks": "8.0.0",
|
||||
"@storybook/components": "8.0.0",
|
||||
"@storybook/core-events": "8.0.0",
|
||||
"@storybook/manager-api": "8.0.0",
|
||||
"@storybook/preview-api": "8.0.0",
|
||||
"@storybook/react": "8.0.0",
|
||||
"@storybook/react-vite": "8.0.0",
|
||||
"@storybook/test": "8.0.0",
|
||||
"@storybook/theming": "8.0.0",
|
||||
"@storybook/types": "8.0.0",
|
||||
"@storybook/vue3": "8.0.0",
|
||||
"@storybook/vue3-vite": "8.0.0",
|
||||
"@testing-library/vue": "8.0.2",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/micromatch": "4.0.6",
|
||||
"@types/node": "20.11.25",
|
||||
"@types/node": "20.11.27",
|
||||
"@types/punycode": "2.1.4",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "9.0.8",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"acorn": "8.11.3",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.6.6",
|
||||
"cypress": "13.7.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"eslint-plugin-vue": "9.23.0",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "13.6.2",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "2.2.2",
|
||||
"msw": "2.2.3",
|
||||
"msw-storybook-addon": "2.0.0-beta.1",
|
||||
"nodemon": "3.1.0",
|
||||
"prettier": "3.2.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"storybook": "8.0.0-beta.6",
|
||||
"storybook": "8.0.0",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "0.34.6",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-component-type-helpers": "1.8.27",
|
||||
"vue-component-type-helpers": "2.0.6",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-tsc": "1.8.27"
|
||||
"vue-tsc": "2.0.6"
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton primary full @click="indieAuthAddNew"><i class="ti ti-plus"></i> New</MkButton>
|
||||
<MkFolder v-for="(client, index) in indieAuthClients" :key="`${index}-${client.createdAt}`" :defaultOpen="!client.createdAt">
|
||||
<MkFolder v-for="(client, index) in indieAuthClients" :key="`${indieAuthTimestamp}-${index}-${client.createdAt ? client.id : 'new'}`" :defaultOpen="!client.createdAt">
|
||||
<template #label>{{ client.name || client.id }}</template>
|
||||
<template #icon>
|
||||
<i v-if="client.id" class="ti ti-key"></i>
|
||||
@ -166,6 +166,72 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>Single Sign-On Service Providers</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton primary full @click="ssoServiceAddNew"><i class="ti ti-plus"></i> New</MkButton>
|
||||
<MkFolder v-for="(service, index) in ssoServices" :key="`${ssoServiceTimestamp}-${index}-${service.createdAt ? service.id : 'new'}`" :defaultOpen="!service.createdAt">
|
||||
<template #label>{{ service.name || service.id }}</template>
|
||||
<template #icon>
|
||||
<i v-if="service.id" class="ti ti-key"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</template>
|
||||
<template v-if="service.name && service.id" #caption>{{ service.id }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="service.id" disabled>
|
||||
<template #label>Service ID</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="service.name">
|
||||
<template #label>Name</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="service.type">
|
||||
<option value="jwt">JWT</option>
|
||||
<option value="saml">SAML</option>
|
||||
</MkRadios>
|
||||
<MkInput v-model="service.issuer">
|
||||
<template #label>Issuer</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="service.audience">
|
||||
<template #label>Audience</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="service.acsUrl">
|
||||
<template #label>Assertion Consumer Service URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="service.publicKey">
|
||||
<template #label>{{ service['useCertificate'] ? 'Public Key' : 'Secret' }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="service.signatureAlgorithm">
|
||||
<template #label>Signature Algorithm</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="service.cipherAlgorithm">
|
||||
<template #label>Cipher Algorithm</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="service.wantAuthnRequestsSigned">
|
||||
<template #label>Want Authn Requests Signed</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="service.wantAssertionsSigned">
|
||||
<template #label>Want Assertions Signed</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="service.useCertificate" :disabled="!!service.createdAt">
|
||||
<template #label>Use Certificate</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-if="service.useCertificate" v-model="service.regenerateCertificate">
|
||||
<template #label>Regenerate Certificate</template>
|
||||
</MkSwitch>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton primary @click="ssoServiceSave(service)"><i class="ti ti-device-floppy"></i> Save</MkButton>
|
||||
<MkButton v-if="service.createdAt" warn @click="ssoServiceDelete(service)"><i class="ti ti-trash"></i> Delete</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkButton v-if="ssoServiceHasMore" :class="$style.more" :disabled="!ssoServiceHasMore" primary rounded @click="ssoServiceFetch()">
|
||||
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@ -208,8 +274,13 @@ const truemailInstance = ref<string | null>(null);
|
||||
const truemailAuthKey = ref<string | null>(null);
|
||||
const bannedEmailDomains = ref<string>('');
|
||||
const indieAuthClients = ref<any[]>([]);
|
||||
const indieAuthTimestamp = ref(0);
|
||||
const indieAuthOffset = ref(0);
|
||||
const indieAuthHasMore = ref(false);
|
||||
const ssoServices = ref<any[]>([]);
|
||||
const ssoServiceTimestamp = ref(0);
|
||||
const ssoServiceOffset = ref(0);
|
||||
const ssoServiceHasMore = ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
@ -274,12 +345,11 @@ function indieAuthFetch(resetOffset = false) {
|
||||
offset: indieAuthOffset.value,
|
||||
limit: 10,
|
||||
}).then(clients => {
|
||||
indieAuthClients.value = indieAuthClients.value.concat(clients.map((client: any) => ({
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
indieAuthClients.value = indieAuthClients.value.concat(clients.map(client => ({
|
||||
...client,
|
||||
redirectUris: client.redirectUris.join('\n'),
|
||||
createdAt: client.createdAt,
|
||||
})));
|
||||
indieAuthTimestamp.value = Date.now();
|
||||
indieAuthHasMore.value = clients.length === 10;
|
||||
indieAuthOffset.value += clients.length;
|
||||
});
|
||||
@ -302,7 +372,9 @@ function indieAuthDelete(client) {
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
indieAuthClients.value = indieAuthClients.value.filter(x => x !== client);
|
||||
misskeyApi('admin/indie-auth/delete', client);
|
||||
os.apiWithDialog('admin/indie-auth/delete', client).then(() => {
|
||||
indieAuthFetch(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -314,11 +386,100 @@ async function indieAuthSave(client) {
|
||||
};
|
||||
|
||||
if (client.createdAt !== undefined) {
|
||||
await misskeyApi('admin/indie-auth/update', params);
|
||||
await os.apiWithDialog('admin/indie-auth/update', params).then(() => {
|
||||
indieAuthFetch(true);
|
||||
});
|
||||
} else {
|
||||
await misskeyApi('admin/indie-auth/create', params);
|
||||
await os.apiWithDialog('admin/indie-auth/create', params).then(() => {
|
||||
indieAuthFetch(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ssoServiceFetch(resetOffset = false) {
|
||||
if (resetOffset) {
|
||||
ssoServices.value = [];
|
||||
ssoServiceOffset.value = 0;
|
||||
}
|
||||
|
||||
misskeyApi('admin/sso/list', {
|
||||
offsetMode: true,
|
||||
offset: ssoServiceOffset.value,
|
||||
limit: 10,
|
||||
}).then(services => {
|
||||
ssoServices.value = ssoServices.value.concat(services.map(service => ({
|
||||
...service,
|
||||
audience: service.audience.join('\n'),
|
||||
})));
|
||||
ssoServiceTimestamp.value = Date.now();
|
||||
ssoServiceHasMore.value = services.length === 10;
|
||||
ssoServiceOffset.value += services.length;
|
||||
});
|
||||
}
|
||||
|
||||
ssoServiceFetch(true);
|
||||
|
||||
function ssoServiceAddNew() {
|
||||
ssoServices.value.unshift({
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'jwt',
|
||||
issuer: '',
|
||||
audience: '',
|
||||
acsUrl: '',
|
||||
publicKey: '',
|
||||
signatureAlgorithm: 'HS256',
|
||||
cipherAlgorithm: '',
|
||||
wantAuthnRequestsSigned: false,
|
||||
wantAssertionsSigned: true,
|
||||
useCertificate: false,
|
||||
regenerateCertificate: false,
|
||||
});
|
||||
}
|
||||
|
||||
function ssoServiceDelete(service) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.deleteAreYouSure({ x: service.id }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
ssoServices.value = ssoServices.value.filter(x => x !== service);
|
||||
os.apiWithDialog('admin/sso/delete', service).then(() => {
|
||||
ssoServiceFetch(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ssoServiceSave(service) {
|
||||
const params = {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
type: service.type,
|
||||
issuer: service.issuer,
|
||||
audience: service.audience.split('\n'),
|
||||
acsUrl: service.acsUrl,
|
||||
secret: service.publicKey,
|
||||
signatureAlgorithm: service.signatureAlgorithm,
|
||||
cipherAlgorithm: service.cipherAlgorithm,
|
||||
wantAuthnRequestsSigned: service.wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned: service.wantAssertionsSigned,
|
||||
};
|
||||
|
||||
if (service.createdAt !== undefined) {
|
||||
await os.apiWithDialog('admin/sso/update', {
|
||||
...params,
|
||||
regenerateCertificate: service.regenerateCertificate,
|
||||
}).then(() => {
|
||||
ssoServiceFetch(true);
|
||||
});
|
||||
} else {
|
||||
await os.apiWithDialog('admin/sso/create', {
|
||||
...params,
|
||||
useCertificate: service.useCertificate,
|
||||
}).then(() => {
|
||||
ssoServiceFetch(true);
|
||||
});
|
||||
}
|
||||
indieAuthFetch(true);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
65
packages/frontend/src/pages/sso.vue
Normal file
65
packages/frontend/src/pages/sso.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<form :class="$style.buttons" :action="`/sso/${kind}/authorize`" accept-charset="utf-8" method="post">
|
||||
<input name="transaction_id" class="mk-input-tr-id-hidden" type="hidden" :value="transactionIdMeta?.content"/>
|
||||
<input name="login_token" class="mk-input-token-hidden" type="hidden" :value="$i.token"/>
|
||||
<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:transaction-id"]');
|
||||
if (transactionIdMeta) {
|
||||
transactionIdMeta.remove();
|
||||
}
|
||||
|
||||
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
|
||||
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
|
||||
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: 'Single Sign-On',
|
||||
icon: 'ti ti-apps',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.loginMessage {
|
||||
text-align: center;
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
</style>
|
@ -264,6 +264,9 @@ const routes: RouteDef[] = [{
|
||||
}, {
|
||||
path: '/oauth/authorize',
|
||||
component: page(() => import('@/pages/oauth.vue')),
|
||||
}, {
|
||||
path: '/sso/:kind/:serviceId',
|
||||
component: page(() => import('@/pages/sso.vue')),
|
||||
}, {
|
||||
path: '/tags/:tag',
|
||||
component: page(() => import('@/pages/tag.vue')),
|
||||
|
@ -26,10 +26,10 @@
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/node": "20.11.25",
|
||||
"@types/node": "20.11.27",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"nodemon": "3.1.0",
|
||||
"typescript": "5.4.2"
|
||||
|
@ -343,6 +343,24 @@ type AdminShowUsersRequest = operations['admin/show-users']['requestBody']['cont
|
||||
// @public (undocumented)
|
||||
type AdminShowUsersResponse = operations['admin/show-users']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoCreateRequest = operations['admin/sso/create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoCreateResponse = operations['admin/sso/create']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoDeleteRequest = operations['admin/sso/delete']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoListRequest = operations['admin/sso/list']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoListResponse = operations['admin/sso/list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSsoUpdateRequest = operations['admin/sso/update']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSuspendUserRequest = operations['admin/suspend-user']['requestBody']['content']['application/json'];
|
||||
|
||||
@ -1276,6 +1294,12 @@ declare namespace entities {
|
||||
AdminRolesUpdateDefaultPoliciesRequest,
|
||||
AdminRolesUsersRequest,
|
||||
AdminRolesUsersResponse,
|
||||
AdminSsoCreateRequest,
|
||||
AdminSsoCreateResponse,
|
||||
AdminSsoDeleteRequest,
|
||||
AdminSsoListRequest,
|
||||
AdminSsoListResponse,
|
||||
AdminSsoUpdateRequest,
|
||||
AnnouncementsRequest,
|
||||
AnnouncementsResponse,
|
||||
AntennasCreateRequest,
|
||||
@ -2457,7 +2481,7 @@ type ModerationLog = {
|
||||
});
|
||||
|
||||
// @public (undocumented)
|
||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
|
||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
|
||||
|
||||
// @public (undocumented)
|
||||
type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json'];
|
||||
@ -2709,7 +2733,7 @@ type PagesUpdateRequest = operations['pages/update']['requestBody']['content']['
|
||||
function parse(acct: string): Acct;
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
|
||||
// @public (undocumented)
|
||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||
|
@ -9,12 +9,12 @@
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@readme/openapi-parser": "2.5.0",
|
||||
"@types/node": "20.11.25",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@types/node": "20.11.27",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"openapi-types": "12.1.3",
|
||||
"openapi-typescript": "6.7.4",
|
||||
"openapi-typescript": "6.7.5",
|
||||
"ts-case-convert": "2.0.7",
|
||||
"tsx": "4.7.1",
|
||||
"typescript": "5.4.2"
|
||||
|
@ -39,9 +39,9 @@
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "20.11.25",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@types/node": "20.11.27",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
|
@ -928,6 +928,50 @@ declare module '../api.js' {
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
request<E extends 'admin/sso/create', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
request<E extends 'admin/sso/delete', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:sso*
|
||||
*/
|
||||
request<E extends 'admin/sso/list', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
request<E extends 'admin/sso/update', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
@ -111,6 +111,12 @@ import type {
|
||||
AdminRolesUpdateDefaultPoliciesRequest,
|
||||
AdminRolesUsersRequest,
|
||||
AdminRolesUsersResponse,
|
||||
AdminSsoCreateRequest,
|
||||
AdminSsoCreateResponse,
|
||||
AdminSsoDeleteRequest,
|
||||
AdminSsoListRequest,
|
||||
AdminSsoListResponse,
|
||||
AdminSsoUpdateRequest,
|
||||
AnnouncementsRequest,
|
||||
AnnouncementsResponse,
|
||||
AntennasCreateRequest,
|
||||
@ -653,6 +659,10 @@ export type Endpoints = {
|
||||
'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse };
|
||||
'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse };
|
||||
'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse };
|
||||
'admin/sso/create': { req: AdminSsoCreateRequest; res: AdminSsoCreateResponse };
|
||||
'admin/sso/delete': { req: AdminSsoDeleteRequest; res: EmptyResponse };
|
||||
'admin/sso/list': { req: AdminSsoListRequest; res: AdminSsoListResponse };
|
||||
'admin/sso/update': { req: AdminSsoUpdateRequest; res: EmptyResponse };
|
||||
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
|
||||
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
|
||||
'antennas/delete': { req: AntennasDeleteRequest; res: EmptyResponse };
|
||||
|
@ -113,6 +113,12 @@ export type AdminRolesUnassignRequest = operations['admin/roles/unassign']['requ
|
||||
export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin/roles/update-default-policies']['requestBody']['content']['application/json'];
|
||||
export type AdminRolesUsersRequest = operations['admin/roles/users']['requestBody']['content']['application/json'];
|
||||
export type AdminRolesUsersResponse = operations['admin/roles/users']['responses']['200']['content']['application/json'];
|
||||
export type AdminSsoCreateRequest = operations['admin/sso/create']['requestBody']['content']['application/json'];
|
||||
export type AdminSsoCreateResponse = operations['admin/sso/create']['responses']['200']['content']['application/json'];
|
||||
export type AdminSsoDeleteRequest = operations['admin/sso/delete']['requestBody']['content']['application/json'];
|
||||
export type AdminSsoListRequest = operations['admin/sso/list']['requestBody']['content']['application/json'];
|
||||
export type AdminSsoListResponse = operations['admin/sso/list']['responses']['200']['content']['application/json'];
|
||||
export type AdminSsoUpdateRequest = operations['admin/sso/update']['requestBody']['content']['application/json'];
|
||||
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
|
||||
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
|
||||
export type AntennasCreateRequest = operations['antennas/create']['requestBody']['content']['application/json'];
|
||||
|
@ -769,6 +769,42 @@ export type paths = {
|
||||
*/
|
||||
post: operations['admin/roles/users'];
|
||||
};
|
||||
'/admin/sso/create': {
|
||||
/**
|
||||
* admin/sso/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
post: operations['admin/sso/create'];
|
||||
};
|
||||
'/admin/sso/delete': {
|
||||
/**
|
||||
* admin/sso/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
post: operations['admin/sso/delete'];
|
||||
};
|
||||
'/admin/sso/list': {
|
||||
/**
|
||||
* admin/sso/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:sso*
|
||||
*/
|
||||
post: operations['admin/sso/list'];
|
||||
};
|
||||
'/admin/sso/update': {
|
||||
/**
|
||||
* admin/sso/update
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
post: operations['admin/sso/update'];
|
||||
};
|
||||
'/announcements': {
|
||||
/**
|
||||
* announcements
|
||||
@ -10287,6 +10323,272 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/sso/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
'admin/sso/create': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
name?: string | null;
|
||||
/** @enum {string} */
|
||||
type: 'saml' | 'jwt';
|
||||
issuer: string;
|
||||
/** @default [] */
|
||||
audience?: string[];
|
||||
acsUrl: string;
|
||||
signatureAlgorithm: string;
|
||||
cipherAlgorithm?: string | null;
|
||||
/** @default false */
|
||||
wantAuthnRequestsSigned?: boolean;
|
||||
/** @default true */
|
||||
wantAssertionsSigned?: boolean;
|
||||
/** @default true */
|
||||
useCertificate: boolean;
|
||||
secret?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
name: string | null;
|
||||
/** @enum {string} */
|
||||
type: 'saml' | 'jwt';
|
||||
issuer: string;
|
||||
audience: string[];
|
||||
acsUrl: string;
|
||||
publicKey: string;
|
||||
signatureAlgorithm: string;
|
||||
cipherAlgorithm?: string | null;
|
||||
wantAuthnRequestsSigned: boolean;
|
||||
wantAssertionsSigned: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @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 Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/sso/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
'admin/sso/delete': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @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 Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/sso/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:sso*
|
||||
*/
|
||||
'admin/sso/list': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
/** @default 0 */
|
||||
offset?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': ({
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
name: string | null;
|
||||
/** @enum {string} */
|
||||
type: 'saml' | 'jwt';
|
||||
issuer: string;
|
||||
audience: string[];
|
||||
acsUrl: string;
|
||||
publicKey: string;
|
||||
signatureAlgorithm: string;
|
||||
cipherAlgorithm?: string | null;
|
||||
wantAuthnRequestsSigned: boolean;
|
||||
wantAssertionsSigned: boolean;
|
||||
})[];
|
||||
};
|
||||
};
|
||||
/** @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 Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/sso/update
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:sso*
|
||||
*/
|
||||
'admin/sso/update': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
id: string;
|
||||
name?: string;
|
||||
issuer?: string;
|
||||
audience?: string[];
|
||||
acsUrl?: string;
|
||||
signatureAlgorithm?: string;
|
||||
cipherAlgorithm?: string;
|
||||
wantAuthnRequestsSigned?: boolean;
|
||||
wantAssertionsSigned?: boolean;
|
||||
regenerateCertificate?: boolean;
|
||||
secret?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @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 Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* announcements
|
||||
* @description No description provided.
|
||||
|
@ -89,6 +89,8 @@ export const permissions = [
|
||||
'write:admin:promo',
|
||||
'write:admin:drive',
|
||||
'read:admin:drive',
|
||||
'write:admin:sso',
|
||||
'read:admin:sso',
|
||||
'write:admin:ad',
|
||||
'read:admin:ad',
|
||||
'write:invite-codes',
|
||||
@ -136,6 +138,9 @@ export const moderationLogTypes = [
|
||||
'createIndieAuthClient',
|
||||
'updateIndieAuthClient',
|
||||
'deleteIndieAuthClient',
|
||||
'createSSOServiceProvider',
|
||||
'updateSSOServiceProvider',
|
||||
'deleteSSOServiceProvider',
|
||||
'createAvatarDecoration',
|
||||
'updateAvatarDecoration',
|
||||
'deleteAvatarDecoration',
|
||||
@ -321,6 +326,19 @@ export type ModerationLogPayloads = {
|
||||
clientId: string;
|
||||
client: any;
|
||||
};
|
||||
createSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
service: any;
|
||||
};
|
||||
updateSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
before: any;
|
||||
after: any;
|
||||
};
|
||||
deleteSSOServiceProvider: {
|
||||
serviceId: string;
|
||||
service: any;
|
||||
};
|
||||
createAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
avatarDecoration: any;
|
||||
|
@ -25,9 +25,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/node": "20.11.25",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@types/node": "20.11.27",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"nodemon": "3.1.0",
|
||||
"typescript": "5.4.2"
|
||||
|
@ -15,8 +15,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"@types/serviceworker": "0.0.84",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"nodemon": "3.1.0",
|
||||
|
3333
pnpm-lock.yaml
3333
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user