Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions api/db/migrations/20260806195442_create_news_page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
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<void> {
console.log('Dropping news table...');

await knex.schema.dropTableIfExists('news');
}
2 changes: 2 additions & 0 deletions api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
memberRoutes,
messageRoutes,
messageboardRoutes,
newsRoutes,
objectInstanceRoutes,
objectRoutes,
placeRoutes,
Expand Down Expand Up @@ -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');
Expand Down
124 changes: 124 additions & 0 deletions api/src/controllers/news.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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<void> {
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 canEditNews(
request: Request,
response: Response,
): Promise<void> {
const session = this.memberService.decryptSession(
request,
response,
);

if (!session) {
return;
}

try {
const canEdit =
await this.memberService.canEditNews(session.id);

response.status(200).json({
canEdit,
});
} catch (error) {
console.log(error);

response.status(400).json({
error: 'A problem occurred while checking News permissions.',
});
}
}
public async updateNews(
request: Request,
response: Response,
): Promise<void> {
const session = this.memberService.decryptSession(
request,
response,
);

if (!session) {
return;
}

const canEditNews =
await this.memberService.canEditNews(session.id);

if (!canEditNews) {
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,
);
1 change: 1 addition & 0 deletions api/src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
48 changes: 48 additions & 0 deletions api/src/repositories/news/news.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Service } from 'typedi';

import { knex } from '../../db';

@Service()
export class NewsRepository {
public async getNews(): Promise<any> {
return knex('news')
.leftJoin(
'member',
'news.updated_by_member_id',
'member.id',
)
.select(
'news.id',
'news.html',
'news.updated_by_member_id',
'news.created_at',
'news.updated_at',
'member.username as updated_by_username',
)
.first();
}

public async updateNews(
html: string,
memberId: number,
): Promise<void> {
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,
});
}
}
1 change: 1 addition & 0 deletions api/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
23 changes: 23 additions & 0 deletions api/src/routes/news.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Router } from 'express';

import { newsController } from '../controllers/news.controller';

export const newsRoutes = Router();

newsRoutes.get(
'/',
(request, response) =>
newsController.getNews(request, response),
);

newsRoutes.get(
'/can-edit',
(request, response) =>
newsController.canEditNews(request, response),
);

newsRoutes.post(
'/',
(request, response) =>
newsController.updateNews(request, response),
);
1 change: 1 addition & 0 deletions api/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
14 changes: 14 additions & 0 deletions api/src/services/member/member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,20 @@ export class MemberService {
return accessLevel;
}

public async canEditNews(memberId: number): Promise<boolean> {
const roleAssignments =
await this.roleAssignmentRepository.getByMemberId(memberId);

const NEWS_ROLES = [
this.roleRepository.roleMap.Admin,
this.roleRepository.roleMap.CityMayor,
this.roleRepository.roleMap.CVNEditor,
];

return !!roleAssignments.find(
assignment => NEWS_ROLES.includes(assignment.role_id),
);
}
/**
* Creates a new member with the given email, username, and password. If successful, distributes
* daily login bonuses, and returns an encoded member token.
Expand Down
32 changes: 32 additions & 0 deletions api/src/services/news/news.service.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return sanitizeHtml(html);
}

public async getNews(): Promise<any> {
return this.newsRepository.getNews();
}

public async updateNews(
html: string,
memberId: number,
): Promise<void> {
return this.newsRepository.updateNews(
html,
memberId,
);
}
}
Binary file added spa/assets/img/anchor.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion spa/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
<img src="/assets/img/ctMinaBanner.gif" />
</div>
<div class="flex h-full items-center px-5" style="justify-content:right">
<img src="/assets/img/news.gif" />
<img
src="/assets/img/news.gif"
class="cursor-pointer"
alt="News"
@click="openWindow('#/news')"
/>
</div>
</div>
</div>
Expand Down
Loading