Skip to content

Commit

Permalink
Merge pull request #12 from yunochi/munochi-dev
Browse files Browse the repository at this point in the history
2024.8.0-munochi.9
  • Loading branch information
yunochi authored Sep 18, 2024
2 parents 9b4f440 + 068661d commit eba145c
Show file tree
Hide file tree
Showing 25 changed files with 558 additions and 228 deletions.
15 changes: 15 additions & 0 deletions Dockerfile_pg_bigm
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM postgres:16-bullseye

RUN apt update
RUN apt install -y postgresql-server-dev-16 make gcc wget libicu-dev

RUN wget https://github.com/pgbigm/pg_bigm/archive/refs/tags/v1.2-20240606.tar.gz
RUN tar zxf v1.2-20240606.tar.gz
RUN cd pg_bigm-1.2-20240606 && make USE_PGXS=1 && make USE_PGXS=1 install

RUN echo shared_preload_libraries='pg_bigm' >> /var/lib/postgresql/data/postgresql.conf

ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 5432
CMD ["postgres"]

5 changes: 5 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,11 @@ createdLists: "Created lists"
createdAntennas: "Created antennas"
noteUpdatedAt: "Edited: {date} {time}"
editHistory: "Edit history"
signinWithPasskey: "Login with passkey"
unknownWebAuthnKey: "It is not authenticated passkey."
verificationFailed: "Failed to verificate passkey."
passwordlessLoginDisabled: "Passkey verification successful, but the passwordless login was not enabled."

_delivery:
status: "Delivery status"
stop: "Suspended"
Expand Down
16 changes: 16 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5084,6 +5084,22 @@ export interface Locale extends ILocale {
* 閉じる
*/
"unfold": string;
/**
* パスキーでログイン
*/
"signinWithPasskey": string;
/**
* 登録していないパスキーです。
*/
"unknownWebAuthnKey": string;
/**
* パスキー検証に失敗しました。
*/
"verificationFailed": string;
/**
* パスキー検証には成功しましたが、パスワードレスログインが無効にしています。
*/
"passwordlessLoginDisabled": string;
"_delivery": {
/**
* 配信状態
Expand Down
4 changes: 4 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,10 @@ noteUpdatedAt: "編集済み: {date} {time}"
editHistory: "修正履歴"
fold: "開く"
unfold: "閉じる"
signinWithPasskey: "パスキーでログイン"
unknownWebAuthnKey: "登録していないパスキーです。"
verificationFailed: "パスキー検証に失敗しました。"
passwordlessLoginDisabled: "パスキー検証には成功しましたが、パスワードレスログインが無効にしています。"

_delivery:
status: "配信状態"
Expand Down
4 changes: 4 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,10 @@ noteUpdatedAt: "편집됨: {date} {time}"
editHistory: "편집 기록"
fold: "펼치기"
unfold: "접기"
signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키 입니다."
verificationFailed: "패스키 검증이 실패했습니다."
passwordlessLoginDisabled: "인증에는 성공했지만, 비밀번호 없이 로그인 설정이 활성화 되어있지 않습니다."

_delivery:
status: "전송 상태"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.8.0+munochi.8",
"version": "2024.8.0+munochi.9",
"codename": "nasubi",
"repository": {
"type": "git",
Expand Down
59 changes: 30 additions & 29 deletions packages/backend/src/core/AvatarDecorationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
import { IsNull } from 'typeorm';
import { UserDetailedNotMe } from 'misskey-js/entities.js';
import type { AvatarDecorationsRepository, InstancesRepository, UsersRepository, MiAvatarDecoration, MiUser } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
Expand All @@ -13,10 +15,9 @@ import { bindThis } from '@/decorators.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { HttpRequestService } from "@/core/HttpRequestService.js";
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
import type { Config } from '@/config.js';
import {IsNull} from "typeorm";

@Injectable()
export class AvatarDecorationService implements OnApplicationShutdown {
Expand Down Expand Up @@ -114,10 +115,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
return appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
query({
url,
...(mode ? { [mode]: '1' } : {}),
}),
query({
url,
...(mode ? { [mode]: '1' } : {}),
}),
);
}

Expand All @@ -134,50 +135,50 @@ export class AvatarDecorationService implements OnApplicationShutdown {

const res = await this.httpRequestService.send(showUserApiUrl, {
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ "username": user.username }),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 'username': user.username }),
});

const userData: any = await res.json();
const userAvatarDecorations = userData.avatarDecorations ?? undefined;
const userData = await res.json() as Partial<UserDetailedNotMe> | undefined;
const userAvatarDecorations = userData?.avatarDecorations;

if (!userAvatarDecorations || userAvatarDecorations.length === 0) {
const updates = {} as Partial<MiUser>;
updates.avatarDecorations = [];
await this.usersRepository.update({id: user.id}, updates);
await this.usersRepository.update({ id: user.id }, updates);
return;
}

const instanceHost = instance?.host;
const instanceHost = instance.host;
const decorationApiUrl = `https://${instanceHost}/api/get-avatar-decorations`;
const allRes = await this.httpRequestService.send(decorationApiUrl, {
method: 'POST',
headers: {"Content-Type": "application/json"},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const allDecorations: any = await allRes.json();
const remoteDecorations = (await allRes.json() as Partial<MiAvatarDecoration[]> | undefined) ?? [];
const updates = {} as Partial<MiUser>;
updates.avatarDecorations = [];
for (const avatarDecoration of userAvatarDecorations) {
for (const userAvatarDecoration of userAvatarDecorations) {
let name;
let description;
const avatarDecorationId = avatarDecoration.id
for (const decoration of allDecorations) {
if (decoration.id == avatarDecorationId) {
name = decoration.name;
description = decoration.description;
const userAvatarDecorationId = userAvatarDecoration.id;
for (const remoteDecoration of remoteDecorations) {
if (remoteDecoration?.id === userAvatarDecorationId) {
name = remoteDecoration.name;
description = remoteDecoration.description;
break;
}
}
const existingDecoration = await this.avatarDecorationsRepository.findOneBy({
host: userHost,
remoteId: avatarDecorationId
remoteId: userAvatarDecorationId,
});
const decorationData = {
name: name,
description: description,
url: this.getProxiedUrl(avatarDecoration.url, 'static'),
remoteId: avatarDecorationId,
url: this.getProxiedUrl(userAvatarDecoration.url),
remoteId: userAvatarDecorationId,
host: userHost,
};
if (existingDecoration == null) {
Expand All @@ -189,18 +190,18 @@ export class AvatarDecorationService implements OnApplicationShutdown {
}
const findDecoration = await this.avatarDecorationsRepository.findOneBy({
host: userHost,
remoteId: avatarDecorationId
remoteId: userAvatarDecorationId,
});

updates.avatarDecorations.push({
id: findDecoration?.id ?? '',
angle: avatarDecoration.angle ?? 0,
flipH: avatarDecoration.flipH ?? false,
offsetX: avatarDecoration.offsetX ?? 0,
offsetY: avatarDecoration.offsetY ?? 0,
angle: userAvatarDecoration.angle ?? 0,
flipH: userAvatarDecoration.flipH ?? false,
offsetX: userAvatarDecoration.offsetX ?? 0,
offsetY: userAvatarDecoration.offsetY ?? 0,
});
}
await this.usersRepository.update({id: user.id}, updates);
await this.usersRepository.update({ id: user.id }, updates);
}

@bindThis
Expand Down
1 change: 0 additions & 1 deletion packages/backend/src/core/NoteDeleteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ export class NoteDeleteService {
}
}


@bindThis
private async getMentionedRemoteUsers(note: MiNote) {
const where = [] as any[];
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/SearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export class SearchService {
}

query
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
.andWhere('note.text LIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
Expand Down
76 changes: 76 additions & 0 deletions packages/backend/src/core/WebAuthnService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,82 @@ export class WebAuthnService {
return authenticationOptions;
}

@bindThis
public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
const relyingParty = await this.getRelyingParty();

const authenticationOptions = await generateAuthenticationOptions({
rpID: relyingParty.rpId,
userVerification: 'preferred',
});

await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);

return authenticationOptions;
}

/**
* Verify Webauthn AuthenticationCredential
* @throws IdentifiableError
* @returns If the challenge is successful, return the user ID. Otherwise, return null.
*/
@bindThis
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);

if (!challenge) {
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
}

await this.redisClient.del(`webauthn:challenge:${context}`);

const key = await this.userSecurityKeysRepository.findOneBy({
id: response.id,
});

if (!key) {
throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
}

const relyingParty = await this.getRelyingParty();

let verification;
try {
verification = await verifyAuthenticationResponse({
response: response,
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},
requireUserVerification: true,
});
} catch (error) {
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
}

const { verified, authenticationInfo } = verification;

if (!verified) {
return null;
}

await this.userSecurityKeysRepository.update({
id: response.id,
}, {
lastUsed: new Date(),
counter: authenticationInfo.newCounter,
credentialDeviceType: authenticationInfo.credentialDeviceType,
credentialBackedUp: authenticationInfo.credentialBackedUp,
});

return key.userId;
}

@bindThis
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
Expand Down
9 changes: 6 additions & 3 deletions packages/backend/src/queue/QueueProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`);
logger.debug(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
Expand Down Expand Up @@ -226,7 +227,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`);
logger.debug(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
Expand Down Expand Up @@ -306,7 +308,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`);
logger.debug(`activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
level: 'error',
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/ServerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';

@Module({
imports: [
Expand All @@ -71,6 +72,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
AuthenticateService,
RateLimiterService,
SigninApiService,
SigninWithPasskeyApiService,
SigninService,
SignupApiService,
StreamingApiServerService,
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/server/api/ApiServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import { ModuleRef } from '@nestjs/core';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
Expand All @@ -17,6 +18,7 @@ import endpoints from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js';
import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';

@Injectable()
Expand All @@ -37,6 +39,7 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
private signinWithPasskeyApiService: SigninWithPasskeyApiService,
) {
//this.createServer = this.createServer.bind(this);
}
Expand Down Expand Up @@ -131,6 +134,12 @@ export class ApiServerService {
};
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));

fastify.post<{
Body: {
credential?: AuthenticationResponseJSON;
};
}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));

fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));

fastify.get('/v1/instance/peers', async (request, reply) => {
Expand Down
Loading

0 comments on commit eba145c

Please sign in to comment.