From d7db4b92cdcdcd3c28e7b12f15d577f3904bbb2e Mon Sep 17 00:00:00 2001 From: phil_00 <162841065+crazifil@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:32:56 +0100 Subject: [PATCH 1/6] Add News backend infrastructure - Add News database migration - Add News repository, service and controller - Register News API routes - Add public News page - Restore original News button --- .../20260806195442_create_news_page.ts | 37 +++++++ api/src/api.ts | 2 + api/src/controllers/news.controller.ts | 98 +++++++++++++++++++ api/src/repositories/index.ts | 1 + api/src/repositories/news/news.repository.ts | 34 +++++++ api/src/routes/index.ts | 1 + api/src/routes/news.routes.ts | 17 ++++ api/src/services/index.ts | 1 + api/src/services/news/news.service.ts | 32 ++++++ spa/src/App.vue | 7 +- spa/src/pages/News.vue | 27 +++++ spa/src/routes.ts | 9 ++ 12 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 api/db/migrations/20260806195442_create_news_page.ts create mode 100644 api/src/controllers/news.controller.ts create mode 100644 api/src/repositories/news/news.repository.ts create mode 100644 api/src/routes/news.routes.ts create mode 100644 api/src/services/news/news.service.ts create mode 100644 spa/src/pages/News.vue diff --git a/api/db/migrations/20260806195442_create_news_page.ts b/api/db/migrations/20260806195442_create_news_page.ts new file mode 100644 index 00000000..f3e6afb6 --- /dev/null +++ b/api/db/migrations/20260806195442_create_news_page.ts @@ -0,0 +1,37 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + console.log('Creating news table...'); + + await knex.schema.createTable('news', table => { + table.increments('id').primary(); + + table + .specificType('html', 'LONGTEXT') + .notNullable(); + + table + .integer('updated_by_member_id') + .unsigned() + .nullable() + .references('id') + .inTable('member') + .onDelete('SET NULL'); + + table + .timestamp('created_at') + .notNullable() + .defaultTo(knex.fn.now()); + + table + .timestamp('updated_at') + .notNullable() + .defaultTo(knex.fn.now()); + }); +} + +export async function down(knex: Knex): Promise { + console.log('Dropping news_page table...'); + + await knex.schema.dropTableIfExists('news'); +} \ No newline at end of file diff --git a/api/src/api.ts b/api/src/api.ts index df874925..849c84a6 100644 --- a/api/src/api.ts +++ b/api/src/api.ts @@ -19,6 +19,7 @@ import { memberRoutes, messageRoutes, messageboardRoutes, + newsRoutes, objectInstanceRoutes, objectRoutes, placeRoutes, @@ -68,6 +69,7 @@ app.use('/api/blackmarket', blackmarketRoutes); app.use('/api/fleamarket', fleamarketRoutes); app.use('/api/club', clubRoutes); app.use('/api/vote', voteRoutes); +app.use('/api/news', newsRoutes); app.use((request, response, next) => { const error = new Error('Not found'); diff --git a/api/src/controllers/news.controller.ts b/api/src/controllers/news.controller.ts new file mode 100644 index 00000000..26549591 --- /dev/null +++ b/api/src/controllers/news.controller.ts @@ -0,0 +1,98 @@ +import { Request, Response } from 'express'; +import { Container } from 'typedi'; + +import { + MemberService, + NewsService, +} from '../services'; + +class NewsController { + constructor( + private memberService: MemberService, + private newsService: NewsService, + ) {} + + public async getNews( + request: Request, + response: Response, + ): Promise { + try { + const news = await this.newsService.getNews(); + + response.status(200).json({ + news: news || null, + }); + } catch (error) { + console.log(error); + + response.status(400).json({ + error: 'A problem occurred while trying to fetch the News page.', + }); + } + } + + public async updateNews( + request: Request, + response: Response, + ): Promise { + const session = this.memberService.decryptSession( + request, + response, + ); + + if (!session) { + return; + } + + const accessLevel = await this.memberService.getAccessLevel( + session.id, + ); + + if (!accessLevel.includes('admin')) { + response.status(403).json({ + error: 'Access Denied', + }); + + return; + } + + const uncleanHtml = request.body.html; + + if (typeof uncleanHtml !== 'string') { + response.status(400).json({ + error: 'News HTML is required.', + }); + + return; + } + + const cleanHtml = await this.newsService.sanitize( + uncleanHtml, + ); + + try { + await this.newsService.updateNews( + cleanHtml, + session.id, + ); + + response.status(200).json({ + success: 'News updated', + }); + } catch (error) { + console.log(error); + + response.status(400).json({ + error: 'A problem occurred while trying to update the News page.', + }); + } + } +} + +const memberService = Container.get(MemberService); +const newsService = Container.get(NewsService); + +export const newsController = new NewsController( + memberService, + newsService, +); \ No newline at end of file diff --git a/api/src/repositories/index.ts b/api/src/repositories/index.ts index 7936f133..13ee3edc 100644 --- a/api/src/repositories/index.ts +++ b/api/src/repositories/index.ts @@ -21,3 +21,4 @@ export * from './wallet/wallet.repository'; export * from './messageboard/messageboard.repository'; export * from './inbox/inbox.repository'; export * from './virtual_pet/virtual-pet.repository'; +export * from './news/news.repository'; \ No newline at end of file diff --git a/api/src/repositories/news/news.repository.ts b/api/src/repositories/news/news.repository.ts new file mode 100644 index 00000000..7184d47d --- /dev/null +++ b/api/src/repositories/news/news.repository.ts @@ -0,0 +1,34 @@ +import { Service } from 'typedi'; + +import { knex } from '../../db'; + +@Service() +export class NewsRepository { + public async getNews(): Promise { + return knex('news').first(); + } + + public async updateNews( + html: string, + memberId: number, + ): Promise { + const existingNews = await knex('news').first(); + + if (existingNews) { + await knex('news') + .where('id', existingNews.id) + .update({ + html, + updated_by_member_id: memberId, + updated_at: knex.fn.now(), + }); + + return; + } + + await knex('news').insert({ + html, + updated_by_member_id: memberId, + }); + } +} \ No newline at end of file diff --git a/api/src/routes/index.ts b/api/src/routes/index.ts index f3da49a6..f1099528 100644 --- a/api/src/routes/index.ts +++ b/api/src/routes/index.ts @@ -10,6 +10,7 @@ export * from './hood.routes'; export * from './mall.routes'; export * from './member.routes'; export * from './message.routes'; +export * from './news.routes'; export * from './object-instance.routes'; export * from './object.routes'; export * from './place.routes'; diff --git a/api/src/routes/news.routes.ts b/api/src/routes/news.routes.ts new file mode 100644 index 00000000..4c56fb8d --- /dev/null +++ b/api/src/routes/news.routes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; + +import { newsController } from '../controllers/news.controller'; + +export const newsRoutes = Router(); + +newsRoutes.get( + '/', + (request, response) => + newsController.getNews(request, response), +); + +newsRoutes.put( + '/', + (request, response) => + newsController.updateNews(request, response), +); \ No newline at end of file diff --git a/api/src/services/index.ts b/api/src/services/index.ts index 9c0b10ec..6923fcd4 100644 --- a/api/src/services/index.ts +++ b/api/src/services/index.ts @@ -19,3 +19,4 @@ export * from './wallet/wallet.service'; export * from './messageboard/messageboard.service'; export * from './inbox/inbox.service'; export * from './vote/vote.service'; +export * from './news/news.service'; \ No newline at end of file diff --git a/api/src/services/news/news.service.ts b/api/src/services/news/news.service.ts new file mode 100644 index 00000000..4d05afb9 --- /dev/null +++ b/api/src/services/news/news.service.ts @@ -0,0 +1,32 @@ +import { Service } from 'typedi'; + +import sanitizeHtml from 'sanitize-html'; + +import { NewsRepository } from '../../repositories/news/news.repository'; + +@Service() +export class NewsService { + constructor( + private newsRepository: NewsRepository, + ) {} + + public async sanitize( + html: string, + ): Promise { + return sanitizeHtml(html); + } + + public async getNews(): Promise { + return this.newsRepository.getNews(); + } + + public async updateNews( + html: string, + memberId: number, + ): Promise { + return this.newsRepository.updateNews( + html, + memberId, + ); + } +} \ No newline at end of file diff --git a/spa/src/App.vue b/spa/src/App.vue index a4b7171e..b762adec 100644 --- a/spa/src/App.vue +++ b/spa/src/App.vue @@ -12,7 +12,12 @@
- + News
diff --git a/spa/src/pages/News.vue b/spa/src/pages/News.vue new file mode 100644 index 00000000..59533608 --- /dev/null +++ b/spa/src/pages/News.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/spa/src/routes.ts b/spa/src/routes.ts index a32e989c..f3d89182 100644 --- a/spa/src/routes.ts +++ b/spa/src/routes.ts @@ -1,4 +1,5 @@ import HomePage from "./pages/HomePage.vue"; +import NewsPage from "./pages/News.vue"; import AboutPage from "./pages/AboutPage.vue"; import ConstitutionPage from './pages/Constitution.vue'; import RulesRegulationsPage from "./pages/RulesandRegulationPage.vue"; @@ -105,6 +106,14 @@ export default [ title: "Welcome to Cybertown", }, }, + { + path: "/News", + component: NewsPage, + name: "news", + meta: { + title: "Cybertown Virtual News", + }, + }, { path: "/about", component: AboutPage, From 18baef9a362cb8892e91fd671d6b04c8b0dccf0b Mon Sep 17 00:00:00 2001 From: phil_00 <162841065+crazifil@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:50:39 +0100 Subject: [PATCH 2/6] Rename news_page table to news Renaming table to fit naming convention --- .../20260806195442_create_news_page.ts | 2 +- spa/src/pages/News.vue | 80 ++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/api/db/migrations/20260806195442_create_news_page.ts b/api/db/migrations/20260806195442_create_news_page.ts index f3e6afb6..d6a134db 100644 --- a/api/db/migrations/20260806195442_create_news_page.ts +++ b/api/db/migrations/20260806195442_create_news_page.ts @@ -31,7 +31,7 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - console.log('Dropping news_page table...'); + console.log('Dropping news table...'); await knex.schema.dropTableIfExists('news'); } \ No newline at end of file diff --git a/spa/src/pages/News.vue b/spa/src/pages/News.vue index 59533608..c327e4cb 100644 --- a/spa/src/pages/News.vue +++ b/spa/src/pages/News.vue @@ -1,27 +1,87 @@ \ No newline at end of file From 60fe20a32b31bf7206684b2eee046e4a5e6ee3e9 Mon Sep 17 00:00:00 2001 From: phil_00 <162841065+crazifil@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:17:08 +0100 Subject: [PATCH 3/6] Add News Admin Editor - Add News editor to the admin panel - Load News content from the API - Save News updates to the database - Add live preview - Connect public News page to backend --- api/src/routes/news.routes.ts | 2 +- spa/src/pages/admin/admin.vue | 3 + spa/src/pages/admin/news/editor.vue | 112 ++++++++++++++++++++++++++++ spa/src/routes.ts | 9 +++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 spa/src/pages/admin/news/editor.vue diff --git a/api/src/routes/news.routes.ts b/api/src/routes/news.routes.ts index 4c56fb8d..e468abf5 100644 --- a/api/src/routes/news.routes.ts +++ b/api/src/routes/news.routes.ts @@ -10,7 +10,7 @@ newsRoutes.get( newsController.getNews(request, response), ); -newsRoutes.put( +newsRoutes.post( '/', (request, response) => newsController.updateNews(request, response), diff --git a/spa/src/pages/admin/admin.vue b/spa/src/pages/admin/admin.vue index 7ed83606..efff7dfd 100644 --- a/spa/src/pages/admin/admin.vue +++ b/spa/src/pages/admin/admin.vue @@ -18,6 +18,9 @@
Roles
+
+ News +
Places
diff --git a/spa/src/pages/admin/news/editor.vue b/spa/src/pages/admin/news/editor.vue new file mode 100644 index 00000000..96348d22 --- /dev/null +++ b/spa/src/pages/admin/news/editor.vue @@ -0,0 +1,112 @@ +