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 848631823..6926ed918 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 bf55cc2b3..c28436ed5 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 7b5b50fed..0b61ff6e3 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 64fdef294..3c87bf7ab 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 ad3c616b8..fb67ea898 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 e633a57bd..218729882 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 041bdfb11..2b973ae55 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 000000000..66854cb02 --- /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 000000000..1f530d7ca --- /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 2c883e0c3..f8249ffcd 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 8dcc1d1eb..4c3aa765e 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 aa00303a1..523ab1303 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 c8b012171..e2a779a52 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 000000000..6d41d38a9 --- /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 9d08e4985..6ce453ef3 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 000000000..c880df752 --- /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 000000000..5f79bdbd0 --- /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 b1b6b2a77..57b6aba9a 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 9bec319af..06653324d 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 2b973ae55..5a3f40a6d 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 049e91289..3fd10fc44 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 5a3f40a6d..0f786a6b1 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 000000000..27fb99f18 --- /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 25ebacaa7..9a1437366 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 2a861adb0..d458a0eeb 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 0630ed3d8..8b914c5ee 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 675748d54..e90102921 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 703506a78..50f81376e 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 000000000..4297e56c3 --- /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 5972a02de..974c4345b 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 6ee3ee8d2..4e237c218 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 2e49f1a64..bcb0d7f2b 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 523ab1303..39bf6ef2d 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 b073a38eb..bc7fc91d3 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 cabd83937..0dd073409 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -1,6 +1,7 @@