Skip to content

Commit

Permalink
Merge pull request #59 from team-shahu/feat/login-notification
Browse files Browse the repository at this point in the history
feat: ログイン通知周りをいい感じに
  • Loading branch information
chan-mai authored Dec 26, 2024
2 parents d5f3583 + 22e36b9 commit 334c365
Show file tree
Hide file tree
Showing 14 changed files with 93 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
- ドライブから削除したファイルをオブジェクトストレージからも葬るように https://github.com/team-shahu/misskey/pull/49 https://github.com/team-shahu/misskey/pull/52
- えもえもローディング画面 https://github.com/team-shahu/misskey/pull/55
- セミパブリックモードの追加 https://github.com/team-shahu/misskey/pull/57
- ログイン通知周りの改良 https://github.com/team-shahu/misskey/pull/59
## Special Thanks
- [Misskey](https://github.com/misskey-dev/misskey)
- [にる村](https://github.com/n1lsqn/misskey)
Expand Down
4 changes: 4 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2469,6 +2469,10 @@ _notification:
flushNotification: "Clear notifications"
exportOfXCompleted: "Export of {x} has been completed"
login: "Someone logged in"
loginDescription: "Logged in from {ip}.\nIf this is an unauthorized device, please log out of all devices through “{text}” for security reasons."
loginFailed: "Login failed."
loginFailedDescription: "Failed login from {ip}. If a login is being attempted from an IP other than your own, please change your password in the settings for security reasons."

_types:
all: "All"
note: "New notes"
Expand Down
14 changes: 14 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9647,6 +9647,20 @@ export interface Locale extends ILocale {
* ログインがありました
*/
"login": string;
/**
* {ip}でログインされました。
* 承認されていない機器であれば、セキュリティのために「{text}」を通じてすべての機器でログアウトを行ってください。
*/
"loginDescription": ParameterizedString<"ip" | "text">;
/**
* ログインに失敗しました
*/
"loginFailed": string;
/**
* {ip}からのログインに失敗しました。
* 自身のip以外でのログインが行われている場合、セキュリティのために設定にてパスワードを変更してください。
*/
"loginFailedDescription": ParameterizedString<"ip">;
"_types": {
/**
* すべて
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,9 @@ _notification:
flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました"
login: "ログインがありました"
loginDescription: "{ip}でログインされました。\n承認されていない機器であれば、セキュリティのために「{text}」を通じてすべての機器でログアウトを行ってください。"
loginFailed: "ログインに失敗しました"
loginFailedDescription: "{ip}からのログインに失敗しました。\n自身のip以外でのログインが行われている場合、セキュリティのために設定にてパスワードを変更してください。"

_types:
all: "すべて"
Expand Down
10 changes: 8 additions & 2 deletions packages/backend/src/core/entities/NotificationEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],

options: {
checkValidNotifier?: boolean;
},
Expand Down Expand Up @@ -193,6 +193,12 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'scheduledNoteFailed' ? {
reason: notification.reason,
} : {}),
...(notification.type === 'login' ? {
ip: notification.userIp,
} : {}),
...(notification.type === 'loginFailed' ? {
ip: notification.userIp,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
Expand Down Expand Up @@ -261,7 +267,7 @@ export class NotificationEntityService implements OnModuleInit {
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],

options: {
checkValidNotifier?: boolean;
},
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export type MiNotification = {
type: 'login';
id: string;
createdAt: string;
userIp: string;
} | {
type: 'loginFailed';
id: string;
createdAt: string;
userIp: string;
} | {
type: 'app';
id: string;
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/models/json-schema/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,24 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
enum: ['login'],
},
ip: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['loginFailed'],
},
ip: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/server/api/SigninApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class SigninApiService {
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
private captchaService: CaptchaService,
private notificationService: NotificationService,
) {
}

Expand Down Expand Up @@ -167,6 +169,11 @@ export class SigninApiService {
success: false,
});

// ログインに失敗したことを通知
await this.notificationService.createNotification(user.id, 'loginFailed', {
userIp: request.ip,
});

return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};

Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/server/api/SigninService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export class SigninService {
@bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
this.notificationService.createNotification(user.id, 'login', {});
this.notificationService.createNotification(user.id, 'login', {
userIp: request.ip,
});

const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* achievementEarned - 実績を獲得
* exportCompleted - エクスポートが完了
* login - ログイン
* loginFailed - ログインに失敗
* scheduledNoteFailed - 予約投稿に失敗
* scheduledNotePosted - 予約投稿をノート
* app - アプリ通知
Expand All @@ -38,6 +39,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'loginFailed',
'scheduledNoteFailed',
'scheduledNotePosted',
'app',
Expand Down
1 change: 1 addition & 0 deletions packages/frontend-shared/js/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'loginFailed',
'test',
'app',
'test',
Expand Down
18 changes: 17 additions & 1 deletion packages/frontend/src/components/MkNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'scheduledNotePosted'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'loginFailed', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
Expand All @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
[$style.t_loginFailed]: notification.type === 'loginFailed',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
[$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed',
[$style.t_pollEnded]: notification.type === 'scheduledNotePosted',
Expand All @@ -44,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
<i v-else-if="notification.type === 'loginFailed'" class="ti ti-lock-exclamation"></i>
<template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
Expand All @@ -66,6 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'loginFailed'">{{ i18n.ts._notification.loginFailed }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
Expand Down Expand Up @@ -118,6 +121,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'scheduledNoteFailed'" :class="$style.text">
{{ notification.reason }}
</div>
<MkA v-else-if="notification.type === 'login'" :class="$style.text" to="/settings/security">
<Mfm :text="i18n.tsx._notification.loginDescription({ ip: notification.ip, text: i18n.ts.regenerateLoginToken })"/>
</MkA>
<MkA v-else-if="notification.type === 'loginFailed'" :class="$style.text" to="/settings/security">
<Mfm :text="i18n.tsx._notification.loginFailedDescription({ ip: notification.ip })"/>
</MkA>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
Expand Down Expand Up @@ -387,6 +396,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}

.t_loginFailed {
padding: 3px;
background: var(--eventUnFollow);
pointer-events: none;
}

.tail {
flex: 1;
min-width: 0;
Expand Down Expand Up @@ -414,6 +429,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
display: flex;
width: 100%;
overflow: clip;
opacity: 0.7;
}

.quote {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/pages/settings/notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const $i = signinRequired();

const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];

const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'loginFailed'] satisfies (typeof notificationTypes[number])[] as string[];

const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
Expand Down
8 changes: 8 additions & 0 deletions packages/sw/src/scripts/create-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif

case 'login':
return [i18n.ts._notification.login, {
body: i18n.tsx._notification.loginDescription({ ip: data.body.ip, text: i18n.ts.regenerateLoginToken }),
badge: iconUrl('login-2'),
data,
}];

case 'loginFailed':
return [i18n.ts._notification.loginFailed, {
body: i18n.tsx._notification.loginFailedDescription({ ip: data.body.ip }),
badge: iconUrl('login-2'),
data,
}];
Expand Down

0 comments on commit 334c365

Please sign in to comment.