Merge remote-tracking branch 'misskey-dev/develop' into develop
# Conflicts: # package.json
This commit is contained in:
commit
94a7eca882
249 changed files with 6467 additions and 8437 deletions
|
|
@ -35,10 +35,10 @@ export class NodeinfoServerService {
|
|||
|
||||
@bindThis
|
||||
public getLinks() {
|
||||
return [/* (awaiting release) {
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||
href: config.url + nodeinfo2_1path
|
||||
}, */{
|
||||
return [{
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||
href: this.config.url + nodeinfo2_1path
|
||||
}, {
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
href: this.config.url + nodeinfo2_0path,
|
||||
}];
|
||||
|
|
@ -46,7 +46,7 @@ export class NodeinfoServerService {
|
|||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
const nodeinfo2 = async () => {
|
||||
const nodeinfo2 = async (version: number) => {
|
||||
const now = Date.now();
|
||||
|
||||
const notesChart = await this.notesChart.getChart('hour', 1, null);
|
||||
|
|
@ -73,11 +73,11 @@ export class NodeinfoServerService {
|
|||
|
||||
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const document: any = {
|
||||
software: {
|
||||
name: 'misskey',
|
||||
version: this.config.version,
|
||||
repository: meta.repositoryUrl,
|
||||
},
|
||||
protocols: ['activitypub'],
|
||||
services: {
|
||||
|
|
@ -114,23 +114,36 @@ export class NodeinfoServerService {
|
|||
themeColor: meta.themeColor ?? '#86b300',
|
||||
},
|
||||
};
|
||||
if (version >= 21) {
|
||||
document.software.repository = meta.repositoryUrl;
|
||||
document.software.homepage = meta.repositoryUrl;
|
||||
}
|
||||
return document;
|
||||
};
|
||||
|
||||
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||
|
||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||
const base = await cache.fetch(() => nodeinfo2());
|
||||
const base = await cache.fetch(() => nodeinfo2(21));
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
reply
|
||||
.type(
|
||||
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"',
|
||||
)
|
||||
.header('Cache-Control', 'public, max-age=600');
|
||||
return { version: '2.1', ...base };
|
||||
});
|
||||
|
||||
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
||||
const base = await cache.fetch(() => nodeinfo2());
|
||||
const base = await cache.fetch(() => nodeinfo2(20));
|
||||
|
||||
delete (base as any).software.repository;
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
reply
|
||||
.type(
|
||||
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"',
|
||||
)
|
||||
.header('Cache-Control', 'public, max-age=600');
|
||||
return { version: '2.0', ...base };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const accessDenied = {
|
|||
export class ApiCallService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private userIpHistories: Map<MiUser['id'], Set<string>>;
|
||||
private userIpHistoriesClearIntervalId: NodeJS.Timer;
|
||||
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.userIpsRepository)
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
|||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
|
||||
import * as ep___pagePush from './endpoints/page-push.js';
|
||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||
|
|
@ -631,6 +632,7 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
|
|||
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
|
||||
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
|
||||
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
|
||||
const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
|
||||
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
|
||||
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
||||
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
||||
|
|
@ -982,6 +984,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$notes_userListTimeline,
|
||||
$notifications_create,
|
||||
$notifications_markAllAsRead,
|
||||
$notifications_testNotification,
|
||||
$pagePush,
|
||||
$pages_create,
|
||||
$pages_delete,
|
||||
|
|
|
|||
|
|
@ -3,22 +3,26 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type {
|
||||
SigninsRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { MiLocalUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class SigninApiService {
|
||||
|
|
@ -29,22 +33,16 @@ export class SigninApiService {
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userSecurityKeysRepository)
|
||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.attestationChallengesRepository)
|
||||
private attestationChallengesRepository: AttestationChallengesRepository,
|
||||
|
||||
@Inject(DI.signinsRepository)
|
||||
private signinsRepository: SigninsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private rateLimiterService: RateLimiterService,
|
||||
private signinService: SigninService,
|
||||
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
|
||||
private webAuthnService: WebAuthnService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -55,11 +53,7 @@ export class SigninApiService {
|
|||
username: string;
|
||||
password: string;
|
||||
token?: string;
|
||||
signature?: string;
|
||||
authenticatorData?: string;
|
||||
clientDataJSON?: string;
|
||||
credentialId?: string;
|
||||
challengeId?: string;
|
||||
credential?: AuthenticationResponseJSON;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
|
|
@ -181,64 +175,16 @@ export class SigninApiService {
|
|||
} else {
|
||||
return this.signinService.signin(request, reply, user);
|
||||
}
|
||||
} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
|
||||
} else if (body.credential) {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
return await fail(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
}
|
||||
|
||||
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
|
||||
const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
|
||||
const challenge = await this.attestationChallengesRepository.findOneBy({
|
||||
userId: user.id,
|
||||
id: body.challengeId,
|
||||
registrationChallenge: false,
|
||||
challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'),
|
||||
});
|
||||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
||||
|
||||
if (!challenge) {
|
||||
return await fail(403, {
|
||||
id: '2715a88a-2125-4013-932f-aa6fe72792da',
|
||||
});
|
||||
}
|
||||
|
||||
await this.attestationChallengesRepository.delete({
|
||||
userId: user.id,
|
||||
id: body.challengeId,
|
||||
});
|
||||
|
||||
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
|
||||
return await fail(403, {
|
||||
id: '2715a88a-2125-4013-932f-aa6fe72792da',
|
||||
});
|
||||
}
|
||||
|
||||
const securityKey = await this.userSecurityKeysRepository.findOneBy({
|
||||
id: Buffer.from(
|
||||
body.credentialId
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/'),
|
||||
'base64',
|
||||
).toString('hex'),
|
||||
});
|
||||
|
||||
if (!securityKey) {
|
||||
return await fail(403, {
|
||||
id: '66269679-aeaf-4474-862b-eb761197e046',
|
||||
});
|
||||
}
|
||||
|
||||
const isValid = this.twoFactorAuthenticationService.verifySignin({
|
||||
publicKey: Buffer.from(securityKey.publicKey, 'hex'),
|
||||
authenticatorData: Buffer.from(body.authenticatorData, 'hex'),
|
||||
clientDataJSON,
|
||||
clientData,
|
||||
signature: Buffer.from(body.signature, 'hex'),
|
||||
challenge: challenge.challenge,
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
if (authorized) {
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
return await fail(403, {
|
||||
|
|
@ -252,42 +198,11 @@ export class SigninApiService {
|
|||
});
|
||||
}
|
||||
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (keys.length === 0) {
|
||||
return await fail(403, {
|
||||
id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
|
||||
});
|
||||
}
|
||||
|
||||
// 32 byte challenge
|
||||
const challenge = randomBytes(32).toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
const challengeId = this.idService.genId();
|
||||
|
||||
await this.attestationChallengesRepository.insert({
|
||||
userId: user.id,
|
||||
id: challengeId,
|
||||
challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
|
||||
createdAt: new Date(),
|
||||
registrationChallenge: false,
|
||||
});
|
||||
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
|
||||
|
||||
reply.code(200);
|
||||
return {
|
||||
challenge,
|
||||
challengeId,
|
||||
securityKeys: keys.map(key => ({
|
||||
id: key.id,
|
||||
})),
|
||||
};
|
||||
return authRequest;
|
||||
}
|
||||
// never get here
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
|||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
|
||||
import * as ep___pagePush from './endpoints/page-push.js';
|
||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||
|
|
@ -629,6 +630,7 @@ const eps = [
|
|||
['notes/user-list-timeline', ep___notes_userListTimeline],
|
||||
['notifications/create', ep___notifications_create],
|
||||
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
||||
['notifications/test-notification', ep___notifications_testNotification],
|
||||
['page-push', ep___pagePush],
|
||||
['pages/create', ep___pages_create],
|
||||
['pages/delete', ep___pages_delete],
|
||||
|
|
|
|||
|
|
@ -3,155 +3,86 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promisify } from 'node:util';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import cbor from 'cbor';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
|
||||
|
||||
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
|
||||
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
incorrectPassword: {
|
||||
message: 'Incorrect password.',
|
||||
code: 'INCORRECT_PASSWORD',
|
||||
id: '0d7ec6d2-e652-443e-a7bf-9ee9a0cd77b0',
|
||||
},
|
||||
|
||||
twoFactorNotEnabled: {
|
||||
message: '2fa not enabled.',
|
||||
code: 'TWO_FACTOR_NOT_ENABLED',
|
||||
id: '798d6847-b1ed-4f9c-b1f9-163c42655995',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientDataJSON: { type: 'string' },
|
||||
attestationObject: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
challengeId: { type: 'string' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 30 },
|
||||
credential: { type: 'object' },
|
||||
},
|
||||
required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'],
|
||||
required: ['password', 'name', 'credential'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.userSecurityKeysRepository)
|
||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
|
||||
@Inject(DI.attestationChallengesRepository)
|
||||
private attestationChallengesRepository: AttestationChallengesRepository,
|
||||
|
||||
private webAuthnService: WebAuthnService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8'));
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
throw new Error('2fa not enabled');
|
||||
throw new ApiError(meta.errors.twoFactorNotEnabled);
|
||||
}
|
||||
|
||||
const clientData = JSON.parse(ps.clientDataJSON);
|
||||
|
||||
if (clientData.type !== 'webauthn.create') {
|
||||
throw new Error('not a creation attestation');
|
||||
}
|
||||
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
|
||||
|
||||
const attestation = await cborDecodeFirst(ps.attestationObject);
|
||||
|
||||
const rpIdHash = attestation.authData.slice(0, 32);
|
||||
if (!rpIdHashReal.equals(rpIdHash)) {
|
||||
throw new Error('rpIdHash mismatch');
|
||||
}
|
||||
|
||||
const flags = attestation.authData[32];
|
||||
|
||||
// eslint:disable-next-line:no-bitwise
|
||||
if (!(flags & 1)) {
|
||||
throw new Error('user not present');
|
||||
}
|
||||
|
||||
const authData = Buffer.from(attestation.authData);
|
||||
const credentialIdLength = authData.readUInt16BE(53);
|
||||
const credentialId = authData.slice(55, 55 + credentialIdLength);
|
||||
const publicKeyData = authData.slice(55 + credentialIdLength);
|
||||
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
|
||||
if (publicKey.get(3) !== -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
const procedures = this.twoFactorAuthenticationService.getProcedures();
|
||||
|
||||
if (!(procedures as any)[attestation.fmt]) {
|
||||
throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`);
|
||||
}
|
||||
|
||||
const verificationData = (procedures as any)[attestation.fmt].verify({
|
||||
attStmt: attestation.attStmt,
|
||||
authenticatorData: authData,
|
||||
clientDataHash: clientDataJSONHash,
|
||||
credentialId,
|
||||
publicKey,
|
||||
rpIdHash,
|
||||
});
|
||||
if (!verificationData.valid) throw new Error('signature invalid');
|
||||
|
||||
const attestationChallenge = await this.attestationChallengesRepository.findOneBy({
|
||||
userId: me.id,
|
||||
id: ps.challengeId,
|
||||
registrationChallenge: true,
|
||||
challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'),
|
||||
});
|
||||
|
||||
if (!attestationChallenge) {
|
||||
throw new Error('non-existent challenge');
|
||||
}
|
||||
|
||||
await this.attestationChallengesRepository.delete({
|
||||
userId: me.id,
|
||||
id: ps.challengeId,
|
||||
});
|
||||
|
||||
// Expired challenge (> 5min old)
|
||||
if (
|
||||
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
|
||||
5 * 60 * 1000
|
||||
) {
|
||||
throw new Error('expired challenge');
|
||||
}
|
||||
|
||||
const credentialIdString = credentialId.toString('hex');
|
||||
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
|
||||
|
||||
const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url');
|
||||
await this.userSecurityKeysRepository.insert({
|
||||
id: credentialId,
|
||||
userId: me.id,
|
||||
id: credentialIdString,
|
||||
lastUsed: new Date(),
|
||||
name: ps.name,
|
||||
publicKey: verificationData.publicKey.toString('hex'),
|
||||
publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
|
||||
counter: keyInfo.counter,
|
||||
credentialDeviceType: keyInfo.credentialDeviceType,
|
||||
credentialBackedUp: keyInfo.credentialBackedUp,
|
||||
transports: keyInfo.transports,
|
||||
});
|
||||
|
||||
// Publish meUpdated event
|
||||
|
|
@ -161,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
|
||||
return {
|
||||
id: credentialIdString,
|
||||
id: credentialId,
|
||||
name: ps.name,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,22 +3,38 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promisify } from 'node:util';
|
||||
import * as crypto from 'node:crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import type { UserProfilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
userNotFound: {
|
||||
message: 'User not found.',
|
||||
code: 'USER_NOT_FOUND',
|
||||
id: '652f899f-66d4-490e-993e-6606c8ec04c3',
|
||||
},
|
||||
|
||||
incorrectPassword: {
|
||||
message: 'Incorrect password.',
|
||||
code: 'INCORRECT_PASSWORD',
|
||||
id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba',
|
||||
},
|
||||
|
||||
twoFactorNotEnabled: {
|
||||
message: '2fa not enabled.',
|
||||
code: 'TWO_FACTOR_NOT_ENABLED',
|
||||
id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -29,53 +45,43 @@ export const paramDef = {
|
|||
required: ['password'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.attestationChallengesRepository)
|
||||
private attestationChallengesRepository: AttestationChallengesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
|
||||
private webAuthnService: WebAuthnService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
where: {
|
||||
userId: me.id,
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (profile == null) {
|
||||
throw new ApiError(meta.errors.userNotFound);
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
throw new Error('2fa not enabled');
|
||||
throw new ApiError(meta.errors.twoFactorNotEnabled);
|
||||
}
|
||||
|
||||
// 32 byte challenge
|
||||
const entropy = await randomBytes(32);
|
||||
const challenge = entropy.toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
const challengeId = this.idService.genId();
|
||||
|
||||
await this.attestationChallengesRepository.insert({
|
||||
userId: me.id,
|
||||
id: challengeId,
|
||||
challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
|
||||
createdAt: new Date(),
|
||||
registrationChallenge: true,
|
||||
});
|
||||
|
||||
return {
|
||||
challengeId,
|
||||
challenge,
|
||||
};
|
||||
return await this.webAuthnService.initiateRegistration(
|
||||
me.id,
|
||||
profile.user?.username ?? me.id,
|
||||
profile.user?.name ?? undefined,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,20 @@ import type { UserProfilesRepository } from '@/models/index.js';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
incorrectPassword: {
|
||||
message: 'Incorrect password.',
|
||||
code: 'INCORRECT_PASSWORD',
|
||||
id: '78d6c839-20c9-4c66-b90a-fc0542168b48',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -39,10 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
// Generate user's secret key
|
||||
|
|
|
|||
|
|
@ -10,11 +10,20 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
|
|||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
incorrectPassword: {
|
||||
message: 'Incorrect password.',
|
||||
code: 'INCORRECT_PASSWORD',
|
||||
id: '141c598d-a825-44c8-9173-cfb9d92be493',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -42,10 +51,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
// Make sure we only delete the user's own creds
|
||||
|
|
|
|||
|
|
@ -10,11 +10,20 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import type { UserProfilesRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
incorrectPassword: {
|
||||
message: 'Incorrect password.',
|
||||
code: 'INCORRECT_PASSWORD',
|
||||
id: '7add0395-9901-4098-82f9-4f67af65f775',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -38,10 +47,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await bcrypt.compare(ps.password, profile.password ?? '');
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
}
|
||||
|
||||
await this.userProfilesRepository.update(me.id, {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const meta = {
|
|||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'You do not have edit privilege of the channel.',
|
||||
message: 'You do not have edit privilege of this key.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ export const paramDef = {
|
|||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', maxLength: 1024, default: '' },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
},
|
||||
required: ['name', 'url', 'secret', 'on'],
|
||||
required: ['name', 'url', 'on'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ export const paramDef = {
|
|||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', maxLength: 1024, default: '' },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'],
|
||||
required: ['webhookId', 'name', 'url', 'on', 'active'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.where(`'{"${me.id}"}' <@ note.mentions`)
|
||||
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
|
||||
}))
|
||||
// Avoid scanning primary key index
|
||||
.orderBy('CONCAT(note.id)', 'DESC')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
// パフォーマンス上の利点が無さそう?
|
||||
//.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notifications'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:notifications',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, user) => {
|
||||
this.notificationService.createNotification(user.id, 'test', {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -80,9 +80,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('note.channel', 'channel')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.channelId IS NULL');
|
||||
qb.orWhere('channel.isSensitive = false');
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me, user);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class UserListChannel extends Channel {
|
|||
public static requireCredential = false;
|
||||
private listId: string;
|
||||
public listUsers: MiUser['id'][] = [];
|
||||
private listUsersClock: NodeJS.Timer;
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default class Connection {
|
|||
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||
private fetchIntervalId: NodeJS.Timer | null = null;
|
||||
private fetchIntervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private channelsService: ChannelsService,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import { deepClone } from '@/misc/clone.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import manifest from './manifest.json' assert { type: 'json' };
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||
|
|
@ -52,6 +51,45 @@ const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
|
|||
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
|
||||
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
|
||||
|
||||
const manifest = {
|
||||
'short_name': 'Misskey',
|
||||
'name': 'Misskey',
|
||||
'start_url': '/',
|
||||
'display': 'standalone',
|
||||
'background_color': '#313a42',
|
||||
'theme_color': '#86b300',
|
||||
'icons': [
|
||||
{
|
||||
'src': '/static-assets/icons/192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
},
|
||||
{
|
||||
'src': '/static-assets/icons/512.png',
|
||||
'sizes': '512x512',
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
},
|
||||
{
|
||||
'src': '/static-assets/splash.png',
|
||||
'sizes': '300x300',
|
||||
'type': 'image/png',
|
||||
'purpose': 'any',
|
||||
},
|
||||
],
|
||||
'share_target': {
|
||||
'action': '/share/',
|
||||
'method': 'GET',
|
||||
'enctype': 'application/x-www-form-urlencoded',
|
||||
'params': {
|
||||
'title': 'title',
|
||||
'text': 'text',
|
||||
'url': 'url',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ClientServerService {
|
||||
private logger: Logger;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ doctype html
|
|||
|
||||
//
|
||||
-
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| '_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
Thank you for using Misskey!
|
||||
If you are reading this message... how about joining the development?
|
||||
https://github.com/misskey-dev/misskey
|
||||
|
||||
|
||||
|
||||
html
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ html
|
|||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.25.0')
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.32.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue