Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024.8.0-munochi.9 #12

Merged
merged 25 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
36713ba
[refactor: frontend, note editing] merge both history sub content com…
kdh8219 Sep 6, 2024
89db5d7
[refactor: fronend, note editing] move some codes
kdh8219 Sep 7, 2024
700643a
[fix: frontend, note editing] change default value of collapsed
kdh8219 Sep 8, 2024
380f7df
[chore: frontend, note editing] lint 'scripts/get-note-history.ts'
kdh8219 Sep 8, 2024
fb2337c
Merge pull request #10 from kdh8219/refactor/yunochi/raw-diff-2
yunochi Sep 8, 2024
7d20d43
fix lint
yunochi Sep 8, 2024
0ae980f
♻️ refactor avatardecoration service
yunochi Sep 8, 2024
1ae2473
Sign in with passkey (PoC)
yunochi Sep 12, 2024
afe9709
💄 Added "Login with Passkey" Button
Squarecat-meow Sep 12, 2024
748c139
WebAuthn챌린지에 실패한 경우의 오류 응답 개선
yunochi Sep 12, 2024
58374b1
signinResponse 는 SigninWithPasskeyResponse 의 아래에 넣도록
yunochi Sep 12, 2024
e932673
프론트 fix
yunochi Sep 12, 2024
c6a957b
Fix: Rate limiting key for passkey signin
yunochi Sep 13, 2024
5e38de3
feat(passkey): enhance Passkey sign-in flow and error handling
yunochi Sep 13, 2024
30998ea
Refactor: Streamline 2FA flow and remove redundant Passkey button.
yunochi Sep 13, 2024
3edee7c
💄 added error messages to MkSignin
Squarecat-meow Sep 15, 2024
d9fc000
✏️ added missing break in case statement
Squarecat-meow Sep 15, 2024
708a747
🚨 fix semicolon warning
Squarecat-meow Sep 16, 2024
d286426
Merge pull request #11 from yunochi/passkey-signin
yunochi Sep 16, 2024
6c085e4
비 로그인 유저에게 에러 대신 빈 연합 목록을 반환하도록 수정
yunochi Sep 16, 2024
2b929a8
inbox Queue에서 발생하는 자세한 renderError를 debug레벨로 낮춤
yunochi Sep 16, 2024
f1f2051
큐 failed시 renderError는 debug로 출력하도록 (로그가 너무 시끄러움)
yunochi Sep 16, 2024
80014f7
use LIKE search instead ILIKE (to use pg_bigm index)
yunochi Sep 16, 2024
66a261f
Add Dockerfile for pg_bigm
yunochi Sep 16, 2024
068661d
2024.8.0-munochi.9
yunochi Sep 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading