diff --git a/migration/1633841235323-announcement.ts b/migration/1633841235323-announcement.ts new file mode 100644 index 0000000..6d2c46a --- /dev/null +++ b/migration/1633841235323-announcement.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from 'typeorm'; + +export class Announcement1633841235323 implements MigrationInterface { + name = 'Announcement1633841235323' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TABLE "announcement" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL, "title" character varying(128) NOT NULL, "body" character varying(8192) NOT NULL, "like" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TABLE "announcement"'); + } + +} diff --git a/nodemon.json b/nodemon.json index 88d0c72..fc92d36 100644 --- a/nodemon.json +++ b/nodemon.json @@ -6,5 +6,5 @@ "src/frontend/*" ], "ext": "ts,tsx,pug,scss", - "exec": "run-s build start" + "exec": "run-s build:backend start" } diff --git a/ormconfig.js b/ormconfig.js index caa751d..4555057 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,6 +1,6 @@ const fs = require('fs'); -const entities = require('./built/services/db').entities; +const entities = require('./built/backend/services/db').entities; const config = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf-8'))); @@ -17,4 +17,4 @@ module.exports = { cli: { migrationsDir: 'migration' } -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index f935433..d9a3723 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "run-p build:*", "build:frontend": "webpack", - "build:backend": "tsc", + "build:backend": "run-p build:backend-source build:views build:styles", + "build:backend-source": "tsc", "build:views": "copyfiles -u 1 src/backend/views/*.pug ./built/", "build:styles": "sass styles/:built/assets", "start": "node built/app.js", @@ -41,7 +42,7 @@ "class-transformer": "^0.4.0", "class-validator": "^0.13.1", "css-loader": "^6.2.0", - "dayjs": "^1.10.2", + "dayjs": "^1.10.7", "delay": "^4.4.0", "fibers": "^5.0.0", "i18next": "^20.6.1", diff --git a/src/backend/controllers/announcement.ts b/src/backend/controllers/announcement.ts new file mode 100644 index 0000000..604830c --- /dev/null +++ b/src/backend/controllers/announcement.ts @@ -0,0 +1,104 @@ +/** + * バージョン情報など、サーバーのメタデータを返すAPI + * @author Xeltica + */ + +import { BadRequestError, Body, CurrentUser, Delete, Get, JsonController, NotFoundError, OnUndefined, Param, Post, Put } from 'routing-controllers'; +import { IUser } from '../../common/types/user'; +import { Announcements } from '../models'; +import { AnnounceCreate } from './body/announce-create'; +import { AnnounceUpdate } from './body/announce-update'; +import { IdProp } from './body/id-prop'; + + @JsonController('/announcements') +export class AdminController { + @Get() get() { + const query = Announcements.createQueryBuilder('announcement') + .orderBy('"announcement"."createdAt"', 'DESC'); + + return query.getMany(); + } + + @OnUndefined(204) + @Post() async post(@CurrentUser({ required: true }) user: IUser, @Body({required: true}) {title, body}: AnnounceCreate) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + if (!title || !body) { + throw new BadRequestError(); + } + await Announcements.insert({ + createdAt: new Date(), + title, + body, + }); + } + + @OnUndefined(204) + @Put() async update(@CurrentUser({ required: true }) user: IUser, @Body() {id, title, body}: AnnounceUpdate) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + if (!id || !title || !body) { + throw new BadRequestError(); + } + if (!(await Announcements.findOne(id))) { + throw new NotFoundError(); + } + + await Announcements.update(id, { + title, + body, + }); + } + + @OnUndefined(204) + @Post('/like/:id') async like(@CurrentUser({ required: true }) user: IUser, @Param('id') id: string) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + const idNumber = Number(id); + if (isNaN(idNumber)) { + throw new NotFoundError(); + } + if (!id) { + throw new BadRequestError(); + } + + const announcement = await Announcements.findOne(Number(idNumber)); + + if (!announcement) { + throw new NotFoundError(); + } + + await Announcements.update(id, { + like: announcement.like + 1, + }); + + return announcement.like + 1; + } + + @Delete() async delete(@CurrentUser({ required: true }) user: IUser, @Body() {id}: IdProp) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + + if (!id) { + throw new BadRequestError(); + } + + await Announcements.delete(id); + } + + @Get('/:id') async getDetail(@Param('id') id: string) { + const idNumber = Number(id); + if (isNaN(idNumber)) { + throw new NotFoundError(); + } + const announcement = await Announcements.findOne(idNumber); + if (!announcement) { + throw new NotFoundError(); + } + return announcement; + } +} diff --git a/src/backend/controllers/body/announce-create.ts b/src/backend/controllers/body/announce-create.ts new file mode 100644 index 0000000..16c4bfb --- /dev/null +++ b/src/backend/controllers/body/announce-create.ts @@ -0,0 +1,4 @@ +export class AnnounceCreate { + title: string; + body: string; +} diff --git a/src/backend/controllers/body/announce-update.ts b/src/backend/controllers/body/announce-update.ts new file mode 100644 index 0000000..7b46f20 --- /dev/null +++ b/src/backend/controllers/body/announce-update.ts @@ -0,0 +1,5 @@ +export class AnnounceUpdate { + id: number; + title: string; + body: string; +} diff --git a/src/backend/controllers/body/id-prop.ts b/src/backend/controllers/body/id-prop.ts new file mode 100644 index 0000000..ed090c7 --- /dev/null +++ b/src/backend/controllers/body/id-prop.ts @@ -0,0 +1,3 @@ +export class IdProp { + id: number; +} diff --git a/src/backend/controllers/UserSetting.ts b/src/backend/controllers/body/user-setting.ts similarity index 68% rename from src/backend/controllers/UserSetting.ts rename to src/backend/controllers/body/user-setting.ts index 9315383..7383e7d 100644 --- a/src/backend/controllers/UserSetting.ts +++ b/src/backend/controllers/body/user-setting.ts @@ -1,7 +1,6 @@ import { IsIn, IsOptional } from 'class-validator'; -import { AlertMode, alertModes } from '../../common/types/alert-mode'; -import { visibilities, Visibility } from '../../common/types/visibility'; - +import { AlertMode, alertModes } from '../../../common/types/alert-mode'; +import { visibilities, Visibility } from '../../../common/types/visibility'; export class UserSetting { @IsIn(alertModes) diff --git a/src/backend/controllers/session.ts b/src/backend/controllers/session.ts index 8e7bc6c..eedb57b 100644 --- a/src/backend/controllers/session.ts +++ b/src/backend/controllers/session.ts @@ -9,7 +9,7 @@ import { getScores } from '../functions/get-scores'; import { deleteUser, updateUser } from '../functions/users'; import { User } from '../models/entities/user'; import { sendAlert } from '../services/send-alert'; -import { UserSetting } from './UserSetting'; +import { UserSetting } from './body/user-setting'; @JsonController('/session') export class SessionController { diff --git a/src/backend/functions/users.ts b/src/backend/functions/users.ts index f9ecc5a..4139f96 100644 --- a/src/backend/functions/users.ts +++ b/src/backend/functions/users.ts @@ -48,7 +48,7 @@ export const updateUsersToolsToken = async (user: User | User['id']): Promise => { +export const getUserByToolsToken = (token: string): Promise => { return Users.findOne({ misshaiToken: token }).then(packUser); }; diff --git a/src/backend/models/entities/announcement.ts b/src/backend/models/entities/announcement.ts new file mode 100644 index 0000000..471f60d --- /dev/null +++ b/src/backend/models/entities/announcement.ts @@ -0,0 +1,31 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { IAnnouncement } from '../../../common/types/announcement'; + +@Entity() +export class Announcement implements IAnnouncement { + @PrimaryGeneratedColumn() + public id: number; + + @Column({ + type: 'timestamp without time zone', + }) + public createdAt: Date; + + @Column({ + type: 'varchar', + length: 128, + }) + public title: string; + + @Column({ + type: 'varchar', + length: 8192, + }) + public body: string; + + @Column({ + type: 'integer', + default: 0, + }) + public like: number; +} diff --git a/src/backend/models/index.ts b/src/backend/models/index.ts index 8c60a30..c9bd934 100644 --- a/src/backend/models/index.ts +++ b/src/backend/models/index.ts @@ -1,6 +1,8 @@ import { User } from './entities/user'; import { UsedToken } from './entities/used-token'; import { getRepository } from 'typeorm'; +import { Announcement } from './entities/announcement'; export const Users = getRepository(User); -export const UsedTokens = getRepository(UsedToken); \ No newline at end of file +export const UsedTokens = getRepository(UsedToken); +export const Announcements = getRepository(Announcement); diff --git a/src/backend/services/db.ts b/src/backend/services/db.ts index d779582..8e2f957 100644 --- a/src/backend/services/db.ts +++ b/src/backend/services/db.ts @@ -2,10 +2,12 @@ import { getConnection, createConnection, Connection } from 'typeorm'; import { config } from '../../config'; import { User } from '../models/entities/user'; import { UsedToken } from '../models/entities/used-token'; +import { Announcement } from '../models/entities/announcement'; export const entities = [ User, UsedToken, + Announcement, ]; /** diff --git a/src/common/types/announcement.ts b/src/common/types/announcement.ts new file mode 100644 index 0000000..87e9e41 --- /dev/null +++ b/src/common/types/announcement.ts @@ -0,0 +1,7 @@ +export interface IAnnouncement { + id: number; + createdAt: Date; + title: string; + body: string; + like: number; +} diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 2ce745d..59123e1 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -17,6 +17,7 @@ import { LOCALSTORAGE_KEY_LANG } from './const'; import 'xeltica-ui/dist/css/xeltica-ui.min.css'; import './style.scss'; +import { AnnouncementPage } from './pages/announcement'; document.body.classList.add('dark'); @@ -63,6 +64,7 @@ const AppInner : React.VFC = () => { +