From 9f5123d176aae05c1bbab25c976a20bd0a4bf01b Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 29 Apr 2021 11:18:08 +0900 Subject: [PATCH 01/11] Fix path --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 848631823c..6926ed918f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Configuration files are located in [`/.circleci`](/.circleci). * Your PR should include all source files (e.g. `.png`, `.blend`) of your models (for later editing). * Your PR must include the glTF binary files (`.glb`) of your models. * Add a locale key `room.furnitures.YOUR_ITEM` at [`/locales/ja-JP.yml`](/locales/ja-JP.yml). -* Add a furniture definition at [`/src/client/app/common/scripts/room/furnitures.json5`](/src/client/app/common/scripts/room/furnitures.json5). +* Add a furniture definition at [`src/client/scripts/room/furnitures.json5`](src/client/scripts/room/furnitures.json5). If you have no experience on 3D modeling, we suggest to use the free 3DCG software [Blender](https://www.blender.org/). You can find information on glTF 2.0 at [glTF 2.0 — Blender Manual]( https://docs.blender.org/manual/en/dev/addons/io_scene_gltf2.html). From d7a5efbd36abc90795ad94f43b069131da5b9be5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 29 Apr 2021 12:31:47 +0900 Subject: [PATCH 02/11] Improve usability --- src/client/ui/chat/index.vue | 2 +- src/client/ui/deck/main-column.vue | 2 +- src/client/ui/default.vue | 2 +- src/client/ui/universal.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index bf55cc2b3f..c28436ed5c 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -313,7 +313,7 @@ export default defineComponent({ } }; if (isLink(e.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; const path = this.$route.path; os.contextMenu([{ diff --git a/src/client/ui/deck/main-column.vue b/src/client/ui/deck/main-column.vue index 7b5b50fedc..0b61ff6e3a 100644 --- a/src/client/ui/deck/main-column.vue +++ b/src/client/ui/deck/main-column.vue @@ -64,7 +64,7 @@ export default defineComponent({ } }; if (isLink(e.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; const path = this.$route.path; os.contextMenu([{ diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index 64fdef2947..3c87bf7ab4 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -165,7 +165,7 @@ export default defineComponent({ } }; if (isLink(e.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; const path = this.$route.path; os.contextMenu([{ diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue index ad3c616b8e..fb67ea8985 100644 --- a/src/client/ui/universal.vue +++ b/src/client/ui/universal.vue @@ -191,7 +191,7 @@ export default defineComponent({ } }; if (isLink(e.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; const path = this.$route.path; os.contextMenu([{ From a34d8549d0d590af4d29a097414abfe914f1c613 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 30 Apr 2021 12:55:30 +0900 Subject: [PATCH 03/11] Fix style --- src/client/components/global/url.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/components/global/url.vue b/src/client/components/global/url.vue index e633a57bd8..218729882d 100644 --- a/src/client/components/global/url.vue +++ b/src/client/components/global/url.vue @@ -113,8 +113,6 @@ export default defineComponent({ > .icon { padding-left: 2px; font-size: .9em; - font-weight: 400; - font-style: normal; } > .self { From 6ae642245e0322f194ca5d960f669f33ba38c2fa Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 May 2021 15:05:34 +0900 Subject: [PATCH 04/11] Password reset (#7494) * wip * wip * Update well-known.ts * wip * clean up * Update request-reset-password.ts * Update forgot-password.vue * Update reset-password.ts * Update request-reset-password.ts --- locales/ja-JP.yml | 6 ++ migration/1619942102890-password-reset.ts | 20 +++++ src/client/components/forgot-password.vue | 71 ++++++++++++++++++ src/client/components/signin.vue | 10 ++- src/client/pages/reset-password.vue | 69 ++++++++++++++++++ src/client/router.ts | 1 + src/client/style.scss | 2 +- src/db/postgre.ts | 2 + src/models/entities/password-reset-request.ts | 30 ++++++++ src/models/index.ts | 2 + .../api/endpoints/request-reset-password.ts | 73 +++++++++++++++++++ src/server/api/endpoints/reset-password.ts | 45 ++++++++++++ src/server/well-known.ts | 5 ++ 13 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 migration/1619942102890-password-reset.ts create mode 100644 src/client/components/forgot-password.vue create mode 100644 src/client/pages/reset-password.vue create mode 100644 src/models/entities/password-reset-request.ts create mode 100644 src/server/api/endpoints/request-reset-password.ts create mode 100644 src/server/api/endpoints/reset-password.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 041bdfb11d..2b973ae55f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -7,6 +7,7 @@ search: "検索" notifications: "通知" username: "ユーザー名" password: "パスワード" +forgotPassword: "パスワードを忘れた" fetchingAsApObject: "連合に照会中" ok: "OK" gotIt: "わかった" @@ -748,6 +749,11 @@ recentPosts: "最近の投稿" popularPosts: "人気の投稿" shareWithNote: "ノートで共有" +_forgotPassword: + enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" + ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。" + contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。" + _gallery: my: "自分の投稿" liked: "いいねした投稿" diff --git a/migration/1619942102890-password-reset.ts b/migration/1619942102890-password-reset.ts new file mode 100644 index 0000000000..66854cb025 --- /dev/null +++ b/migration/1619942102890-password-reset.ts @@ -0,0 +1,20 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class passwordReset1619942102890 implements MigrationInterface { + name = 'passwordReset1619942102890' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `); + await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `); + await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`); + await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`); + await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`); + await queryRunner.query(`DROP TABLE "password_reset_request"`); + } + +} diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue new file mode 100644 index 0000000000..1f530d7ca2 --- /dev/null +++ b/src/client/components/forgot-password.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index 2c883e0c32..f8249ffcd6 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -11,6 +11,7 @@ {{ $ts.password }} + {{ signing ? $ts.loggingIn : $ts.login }} @@ -49,8 +50,8 @@ + + diff --git a/src/client/router.ts b/src/client/router.ts index 8dcc1d1eb4..4c3aa765e6 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -23,6 +23,7 @@ export const router = createRouter({ { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:acct/room', props: true, component: page('room/room') }, { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) }, { path: '/announcements', component: page('announcements') }, { path: '/about', component: page('about') }, { path: '/about-misskey', component: page('about-misskey') }, diff --git a/src/client/style.scss b/src/client/style.scss index aa00303a15..523ab13034 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -337,7 +337,7 @@ hr { } ._monolithic_ { - ._section { + ._section:not(:empty) { box-sizing: border-box; padding: var(--root-margin, 32px); diff --git a/src/db/postgre.ts b/src/db/postgre.ts index c8b0121719..e2a779a52d 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -70,6 +70,7 @@ import { Channel } from '../models/entities/channel'; import { ChannelFollowing } from '../models/entities/channel-following'; import { ChannelNotePining } from '../models/entities/channel-note-pining'; import { RegistryItem } from '../models/entities/registry-item'; +import { PasswordResetRequest } from '@/models/entities/password-reset-request'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -169,6 +170,7 @@ export const entities = [ ChannelFollowing, ChannelNotePining, RegistryItem, + PasswordResetRequest, ...charts as any ]; diff --git a/src/models/entities/password-reset-request.ts b/src/models/entities/password-reset-request.ts new file mode 100644 index 0000000000..6d41d38a93 --- /dev/null +++ b/src/models/entities/password-reset-request.ts @@ -0,0 +1,30 @@ +import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id'; +import { User } from './user'; + +@Entity() +export class PasswordResetRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, + }) + public token: string; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; +} diff --git a/src/models/index.ts b/src/models/index.ts index 9d08e49858..6ce453ef33 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -60,6 +60,7 @@ import { MutedNote } from './entities/muted-note'; import { ChannelFollowing } from './entities/channel-following'; import { ChannelNotePining } from './entities/channel-note-pining'; import { RegistryItem } from './entities/registry-item'; +import { PasswordResetRequest } from './entities/password-reset-request'; export const Announcements = getRepository(Announcement); export const AnnouncementReads = getRepository(AnnouncementRead); @@ -122,3 +123,4 @@ export const Channels = getCustomRepository(ChannelRepository); export const ChannelFollowings = getRepository(ChannelFollowing); export const ChannelNotePinings = getRepository(ChannelNotePining); export const RegistryItems = getRepository(RegistryItem); +export const PasswordResetRequests = getRepository(PasswordResetRequest); diff --git a/src/server/api/endpoints/request-reset-password.ts b/src/server/api/endpoints/request-reset-password.ts new file mode 100644 index 0000000000..c880df7527 --- /dev/null +++ b/src/server/api/endpoints/request-reset-password.ts @@ -0,0 +1,73 @@ +import $ from 'cafy'; +import { publishMainStream } from '../../../services/stream'; +import define from '../define'; +import rndstr from 'rndstr'; +import config from '@/config'; +import * as ms from 'ms'; +import { Users, UserProfiles, PasswordResetRequests } from '../../../models'; +import { sendEmail } from '../../../services/send-email'; +import { ApiError } from '../error'; +import { genId } from '@/misc/gen-id'; +import { IsNull } from 'typeorm'; + +export const meta = { + requireCredential: false as const, + + limit: { + duration: ms('1hour'), + max: 3 + }, + + params: { + username: { + validator: $.str + }, + + email: { + validator: $.str + }, + }, + + errors: { + + } +}; + +export default define(meta, async (ps) => { + const user = await Users.findOne({ + usernameLower: ps.username.toLowerCase(), + host: IsNull() + }); + + // 合致するユーザーが登録されていなかったら無視 + if (user == null) { + return; + } + + const profile = await UserProfiles.findOneOrFail(user.id); + + // 合致するメアドが登録されていなかったら無視 + if (profile.email !== ps.email) { + return; + } + + // メアドが認証されていなかったら無視 + if (!profile.emailVerified) { + return; + } + + const token = rndstr('a-z0-9', 64); + + await PasswordResetRequests.insert({ + id: genId(), + createdAt: new Date(), + userId: profile.userId, + token + }); + + const link = `${config.url}/reset-password/${token}`; + + sendEmail(ps.email, 'Password reset requested', + `To reset password, please click this link:
${link}`, + `To reset password, please click this link: ${link}`); +}); diff --git a/src/server/api/endpoints/reset-password.ts b/src/server/api/endpoints/reset-password.ts new file mode 100644 index 0000000000..5f79bdbd00 --- /dev/null +++ b/src/server/api/endpoints/reset-password.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import * as bcrypt from 'bcryptjs'; +import { publishMainStream } from '../../../services/stream'; +import define from '../define'; +import { Users, UserProfiles, PasswordResetRequests } from '../../../models'; +import { ApiError } from '../error'; + +export const meta = { + requireCredential: false as const, + + params: { + token: { + validator: $.str + }, + + password: { + validator: $.str + } + }, + + errors: { + + } +}; + +export default define(meta, async (ps, user) => { + const req = await PasswordResetRequests.findOneOrFail({ + token: ps.token, + }); + + // 発行してから30分以上経過していたら無効 + if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { + throw new Error(); // TODO + } + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.password, salt); + + await UserProfiles.update(req.userId, { + password: hash + }); + + PasswordResetRequests.delete(req.id); +}); diff --git a/src/server/well-known.ts b/src/server/well-known.ts index b1b6b2a771..57b6aba9a0 100644 --- a/src/server/well-known.ts +++ b/src/server/well-known.ts @@ -61,6 +61,11 @@ router.get('/.well-known/nodeinfo', async ctx => { ctx.body = { links }; }); +/* TODO +router.get('/.well-known/change-password', async ctx => { +}); +*/ + router.get(webFingerPath, async ctx => { const fromId = (id: User['id']): Record => ({ id, From e9170e630c35a7669e99f7e6fb73243f171ac7b8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 May 2021 17:02:14 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=8C=E3=83=AA=E3=82=A2=E3=83=AB=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=A0=E3=81=A7=E5=8F=8D=E6=98=A0=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/components/emoji-picker.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue index 9bec319af2..06653324d7 100644 --- a/src/client/components/emoji-picker.vue +++ b/src/client/components/emoji-picker.vue @@ -35,6 +35,7 @@ class="_button" @click="chosen(emoji, $event)" tabindex="0" + :key="emoji" > @@ -104,7 +105,7 @@ export default defineComponent({ return { emojilist: markRaw(emojilist), getStaticImageUrl, - pinned: this.$store.state.reactions, + pinned: this.$store.reactiveState.reactions, width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3, height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2, big: this.asReactionPicker ? isDeviceTouch : false, From 71ebb068f7eb51f473f35d8e6ae7cfbad1e74b62 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 May 2021 17:09:57 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?= =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=82=92?= =?UTF-8?q?=E4=BF=83=E3=81=99=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 1 + src/client/pages/settings/index.vue | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2b973ae55f..5a3f40a6dc 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -748,6 +748,7 @@ gallery: "ギャラリー" recentPosts: "最近の投稿" popularPosts: "人気の投稿" shareWithNote: "ノートで共有" +emailNotConfiguredWarning: "メールアドレスの設定がされていません。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 049e912898..3fd10fc44f 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -10,6 +10,7 @@ {{ $ts.accounts }} + {{ $ts.emailNotConfiguredWarning }} {{ $ts.configure }} {{ $ts.profile }} @@ -58,10 +59,13 @@ import FormLink from '@client/components/form/link.vue'; import FormGroup from '@client/components/form/group.vue'; import FormBase from '@client/components/form/base.vue'; import FormButton from '@client/components/form/button.vue'; +import FormInfo from '@client/components/form/info.vue'; import { scroll } from '@client/scripts/scroll'; import { signout } from '@client/account'; import { unisonReload } from '@client/scripts/unison-reload'; import * as symbols from '@client/symbols'; +import { instance } from '@client/instance'; +import { $i } from '@client/account'; export default defineComponent({ components: { @@ -69,6 +73,7 @@ export default defineComponent({ FormLink, FormGroup, FormButton, + FormInfo, }, props: { @@ -173,6 +178,8 @@ export default defineComponent({ } }); + const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); + return { [symbols.PAGE_INFO]: INFO, page, @@ -182,6 +189,7 @@ export default defineComponent({ onInfo, pageProps, component, + emailNotConfigured, logout: () => { signout(); }, From 18e1efc7ecd3f5a6d774c16f17526d12ae46b2f5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 May 2021 21:15:57 +0900 Subject: [PATCH 07/11] Ad (#7495) * wip * Update ad.vue * Update default.widgets.vue * wip * Create 1620019354680-ad.ts * wip * Update ads.vue * wip * Update ad.vue --- locales/ja-JP.yml | 7 + migration/1620019354680-ad.ts | 18 +++ package.json | 1 - src/client/components/date-separated-list.vue | 22 ++- src/client/components/global/ad.vue | 142 ++++++++++++++++++ src/client/components/index.ts | 4 +- src/client/components/notes.vue | 2 +- src/client/pages/gallery/post.vue | 1 + src/client/pages/instance/ads.vue | 125 +++++++++++++++ src/client/pages/instance/index.vue | 2 + src/client/pages/page.vue | 1 + src/client/scripts/paging.ts | 8 +- src/client/style.scss | 2 + src/client/ui/chat/date-separated-list.vue | 6 +- src/client/ui/default.widgets.vue | 1 + src/db/postgre.ts | 2 + src/models/entities/ad.ts | 53 +++++++ src/models/index.ts | 2 + src/models/repositories/note.ts | 11 +- src/server/api/endpoints/admin/ad/create.ts | 45 ++++++ src/server/api/endpoints/admin/ad/delete.ts | 34 +++++ src/server/api/endpoints/admin/ad/list.ts | 36 +++++ src/server/api/endpoints/admin/ad/update.ts | 59 ++++++++ src/server/api/endpoints/meta.ts | 39 ++++- 24 files changed, 596 insertions(+), 27 deletions(-) create mode 100644 migration/1620019354680-ad.ts create mode 100644 src/client/components/global/ad.vue create mode 100644 src/client/pages/instance/ads.vue create mode 100644 src/models/entities/ad.ts create mode 100644 src/server/api/endpoints/admin/ad/create.ts create mode 100644 src/server/api/endpoints/admin/ad/delete.ts create mode 100644 src/server/api/endpoints/admin/ad/list.ts create mode 100644 src/server/api/endpoints/admin/ad/update.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5a3f40a6dc..0f786a6b14 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -748,6 +748,13 @@ gallery: "ギャラリー" recentPosts: "最近の投稿" popularPosts: "人気の投稿" shareWithNote: "ノートで共有" +ads: "広告" +expiration: "期限" +memo: "メモ" +priority: "優先度" +high: "高" +middle: "中" +low: "低" emailNotConfiguredWarning: "メールアドレスの設定がされていません。" _forgotPassword: diff --git a/migration/1620019354680-ad.ts b/migration/1620019354680-ad.ts new file mode 100644 index 0000000000..27fb99f181 --- /dev/null +++ b/migration/1620019354680-ad.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class ad1620019354680 implements MigrationInterface { + name = 'ad1620019354680' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`); + await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`); + await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`); + await queryRunner.query(`DROP TABLE "ad"`); + } + +} diff --git a/package.json b/package.json index 25ebacaa7c..9a14373667 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "resolutions": { "chokidar": "^3.3.1", "constantinople": "^4.0.1", - "gulp/gulp-cli/yargs/yargs-parser": "5.0.0-security.0", "jsonld/rdf-canonize/node-forge": "0.10.0", "lodash": "^4.17.20" }, diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue index 2a861adb09..d458a0eeb8 100644 --- a/src/client/components/date-separated-list.vue +++ b/src/client/components/date-separated-list.vue @@ -1,5 +1,6 @@ + + diff --git a/src/client/components/index.ts b/src/client/components/index.ts index 0630ed3d8c..8b914c5eec 100644 --- a/src/client/components/index.ts +++ b/src/client/components/index.ts @@ -12,8 +12,10 @@ import url from './global/url.vue'; import i18n from './global/i18n'; import loading from './global/loading.vue'; import error from './global/error.vue'; +import ad from './global/ad.vue'; export default function(app: App) { + app.component('I18n', i18n); app.component('Mfm', mfm); app.component('MkA', a); app.component('MkAcct', acct); @@ -25,5 +27,5 @@ export default function(app: App) { app.component('MkUrl', url); app.component('MkLoading', loading); app.component('MkError', error); - app.component('I18n', i18n); + app.component('MkAd', ad); } diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 675748d540..e90102921a 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -17,7 +17,7 @@ - + diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue index 703506a78d..50f81376ec 100644 --- a/src/client/pages/gallery/post.vue +++ b/src/client/pages/gallery/post.vue @@ -33,6 +33,7 @@ + diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/instance/ads.vue new file mode 100644 index 0000000000..4297e56c37 --- /dev/null +++ b/src/client/pages/instance/ads.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 5972a02de0..974c4345bb 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -23,6 +23,7 @@ {{ $ts.jobQueue }} {{ $ts.files }} {{ $ts.announcements }} + {{ $ts.ads }} {{ $ts.abuseReports }} @@ -102,6 +103,7 @@ export default defineComponent({ case 'queue': return defineAsyncComponent(() => import('./queue.vue')); case 'files': return defineAsyncComponent(() => import('./files.vue')); case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); + case 'ads': return defineAsyncComponent(() => import('./ads.vue')); case 'database': return defineAsyncComponent(() => import('./database.vue')); case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); case 'settings': return defineAsyncComponent(() => import('./settings.vue')); diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue index 6ee3ee8d26..4e237c2186 100644 --- a/src/client/pages/page.vue +++ b/src/client/pages/page.vue @@ -45,6 +45,7 @@
{{ $ts.createdAt }}:
{{ $ts.updatedAt }}:
+ diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts index 2e49f1a64c..bcb0d7f2b0 100644 --- a/src/client/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -91,8 +91,10 @@ export default (opts) => ({ ...params, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, }).then(items => { - for (const item of items) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; markRaw(item); + if (i === 3) item._shouldInsertAd_ = true; } if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { items.pop(); @@ -128,8 +130,10 @@ export default (opts) => ({ untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, }), }).then(items => { - for (const item of items) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; markRaw(item); + if (i === 10) item._shouldInsertAd_ = true; } if (items.length > SECOND_FETCH_LIMIT) { items.pop(); diff --git a/src/client/style.scss b/src/client/style.scss index 523ab13034..39bf6ef2d5 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -11,6 +11,8 @@ @media (max-width: 500px) { --margin: var(--marginHalf); } + + //--ad: rgb(255 169 0 / 10%); } ::selection { diff --git a/src/client/ui/chat/date-separated-list.vue b/src/client/ui/chat/date-separated-list.vue index b073a38eb1..bc7fc91d38 100644 --- a/src/client/ui/chat/date-separated-list.vue +++ b/src/client/ui/chat/date-separated-list.vue @@ -42,11 +42,7 @@ export default defineComponent({ if ( i != this.items.length - 1 && - new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() && - !item._prId_ && - !this.items[i + 1]._prId_ && - !item._featuredId_ && - !this.items[i + 1]._featuredId_ + new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() ) { const separator = h('div', { class: 'separator', diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue index cabd83937e..0dd073409b 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -1,6 +1,7 @@