From 49749b46c4c8914947e0950069bcf4431f157c4f Mon Sep 17 00:00:00 2001
From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 12 Apr 2023 12:52:14 +0900
Subject: [PATCH] =?UTF-8?q?feat(server):=20Misskey=20Web=E3=81=A7=E3=83=A6?=
 =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=83=95=E3=83=AC=E3=83=B3=E3=83=89?=
 =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=81=AA=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=9A?=
 =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E5=87=BA=E3=81=99=20(#10590)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* (add) user-friendly error page

* Update CHANGELOG.md

* (add) cache-control header

* Add ClientLoggerService

* Log params and query

* remove error stack on client

* fix pug

* 文面を調整

* :art]

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 CHANGELOG.md                                  |   2 +
 gulpfile.js                                   |   2 +-
 packages/backend/src/server/ServerModule.ts   |   2 +
 .../src/server/web/ClientLoggerService.ts     |  14 +++
 .../src/server/web/ClientServerService.ts     |  24 ++++
 packages/backend/src/server/web/error.css     | 110 ++++++++++++++++++
 .../backend/src/server/web/views/error.pug    |  65 +++++++++++
 7 files changed, 218 insertions(+), 1 deletion(-)
 create mode 100644 packages/backend/src/server/web/ClientLoggerService.ts
 create mode 100644 packages/backend/src/server/web/error.css
 create mode 100644 packages/backend/src/server/web/views/error.pug

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd195790fa..d992cc94a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@
 -
 
 ### Server
+- Misskey Webでのサーバーサイドエラー画面を改善
+- Misskey Webでのサーバーサイドエラーのログが残るように
 - ノート作成時のアンテナ追加パフォーマンスを改善
 - フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
 
diff --git a/gulpfile.js b/gulpfile.js
index a04ab4c1ad..6507aad60e 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -45,7 +45,7 @@ gulp.task('build:backend:script', () => {
 });
 
 gulp.task('build:backend:style', () => {
-	return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
+	return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
 		.pipe(cssnano({
 			zindex: false
 		}))
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index c41e805504..da86b2c1d3 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -34,6 +34,7 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
 import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
 import { UserListChannelService } from './api/stream/channels/user-list.js';
 import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
+import { ClientLoggerService } from './web/ClientLoggerService.js';
 import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
 
 @Module({
@@ -43,6 +44,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.
 	],
 	providers: [
 		ClientServerService,
+		ClientLoggerService,
 		FeedService,
 		UrlPreviewService,
 		ActivityPubServerService,
diff --git a/packages/backend/src/server/web/ClientLoggerService.ts b/packages/backend/src/server/web/ClientLoggerService.ts
new file mode 100644
index 0000000000..6a882aa766
--- /dev/null
+++ b/packages/backend/src/server/web/ClientLoggerService.ts
@@ -0,0 +1,14 @@
+import { Injectable } from '@nestjs/common';
+import type Logger from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+
+@Injectable()
+export class ClientLoggerService {
+	public logger: Logger;
+
+	constructor(
+		private loggerService: LoggerService,
+	) {
+		this.logger = this.loggerService.getLogger('client');
+	}
+}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 99ae1b7af6..50b23a0682 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -1,6 +1,7 @@
 import { dirname } from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { Inject, Injectable } from '@nestjs/common';
+import { v4 as uuid } from 'uuid';
 import { createBullBoard } from '@bull-board/api';
 import { BullAdapter } from '@bull-board/api/bullAdapter.js';
 import { FastifyAdapter } from '@bull-board/fastify';
@@ -26,6 +27,7 @@ import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityServi
 import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
 import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
+import type Logger from '@/logger.js';
 import { deepClone } from '@/misc/clone.js';
 import { bindThis } from '@/decorators.js';
 import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
@@ -34,6 +36,7 @@ import manifest from './manifest.json' assert { type: 'json' };
 import { FeedService } from './FeedService.js';
 import { UrlPreviewService } from './UrlPreviewService.js';
 import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
+import { ClientLoggerService } from './ClientLoggerService.js';
 
 const _filename = fileURLToPath(import.meta.url);
 const _dirname = dirname(_filename);
@@ -46,6 +49,8 @@ const viteOut = `${_dirname}/../../../../../built/_vite_/`;
 
 @Injectable()
 export class ClientServerService {
+	private logger: Logger;
+
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
@@ -85,6 +90,7 @@ export class ClientServerService {
 		private urlPreviewService: UrlPreviewService,
 		private feedService: FeedService,
 		private roleService: RoleService,
+		private clientLoggerService: ClientLoggerService,
 
 		@Inject('queue:system') public systemQueue: SystemQueue,
 		@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@@ -649,6 +655,24 @@ export class ClientServerService {
 			return await renderBase(reply);
 		});
 
+		fastify.setErrorHandler(async (error, request, reply) => {
+			const errId = uuid();
+			this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, {
+				path: request.routerPath,
+				params: request.params,
+				query: request.query,
+				code: error.name,
+				stack: error.stack,
+				id: errId,
+			});
+			reply.code(500);
+			reply.header('Cache-Control', 'max-age=10, must-revalidate');
+			return await reply.view('error', {
+				code: error.code,
+				id: errId,
+			});
+		});
+
 		done();
 	}
 }
diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css
new file mode 100644
index 0000000000..ab913f7a9f
--- /dev/null
+++ b/packages/backend/src/server/web/error.css
@@ -0,0 +1,110 @@
+* {
+    font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+}
+
+#misskey_app,
+#splash {
+    display: none !important;
+}
+
+body,
+html {
+    background-color: #222;
+    color: #dfddcc;
+    justify-content: center;
+    margin: auto;
+    padding: 10px;
+    text-align: center;
+}
+
+button {
+    border-radius: 999px;
+    padding: 0px 12px 0px 12px;
+    border: none;
+    cursor: pointer;
+    margin-bottom: 12px;
+}
+
+.button-big {
+    background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
+    line-height: 50px;
+}
+
+.button-big:hover {
+    background: rgb(153, 204, 0);
+}
+
+.button-small {
+    background: #444;
+    line-height: 40px;
+}
+
+.button-small:hover {
+    background: #555;
+}
+
+.button-label-big {
+    color: #222;
+    font-weight: bold;
+    font-size: 20px;
+    padding: 12px;
+}
+
+.button-label-small {
+    color: rgb(153, 204, 0);
+    font-size: 16px;
+    padding: 12px;
+}
+
+a {
+    color: rgb(134, 179, 0);
+    text-decoration: none;
+}
+
+p,
+li {
+    font-size: 16px;
+}
+
+.dont-worry,
+#msg {
+    font-size: 18px;
+}
+
+.icon-warning {
+    color: #dec340;
+    height: 4rem;
+    padding-top: 2rem;
+}
+
+h1 {
+    font-size: 32px;
+}
+
+code {
+    display: block;
+    font-family: Fira, FiraCode, monospace;
+    background: #333;
+    padding: 0.5rem 1rem;
+    max-width: 40rem;
+    border-radius: 10px;
+    justify-content: center;
+    margin: auto;
+    white-space: pre-wrap;
+    word-break: break-word;
+}
+
+summary {
+    cursor: pointer;
+}
+
+summary > * {
+    display: inline;
+    white-space: pre-wrap;
+}
+
+@media screen and (max-width: 500px) {
+    details {
+        width: 50%;
+    }
+}
\ No newline at end of file
diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug
new file mode 100644
index 0000000000..b177ae4110
--- /dev/null
+++ b/packages/backend/src/server/web/views/error.pug
@@ -0,0 +1,65 @@
+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
+
+	head
+		meta(charset='utf-8')
+		meta(name='viewport' content='width=device-width, initial-scale=1')
+		meta(name='application-name' content='Misskey')
+		meta(name='referrer' content='origin')
+
+		title
+			block title
+				= 'An error has occurred... | Misskey'
+
+		style
+			include ../error.css
+
+body
+	svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round")
+		path(stroke="none", d="M0 0h24v24H0z", fill="none")
+		path(d="M12 9v2m0 4v.01")
+		path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
+	
+	h1 An error has occurred!
+
+	button.button-big(onclick="location.reload();")
+		span.button-label-big Refresh
+	
+	p.dont-worry Don't worry, it's (probably) not your fault.
+
+	p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
+
+	div#errors
+		code.
+			ERROR CODE: #{code}
+			ERROR ID: #{id}
+
+	p You may also try the following options:
+
+	p Update your os and browser.
+	p Disable an adblocker.
+
+	a(href="/flush")
+		button.button-small
+			span.button-label-small Clear preferences and cache
+	br
+	a(href="/cli")
+		button.button-small
+			span.button-label-small Start the simple client
+	br
+	a(href="/bios")
+		button.button-small
+			span.button-label-small Start the repair tool