diff --git a/src/v2/controller/post.controller.ts b/src/v2/controller/post.controller.ts index 810c36e..9fb5b34 100644 --- a/src/v2/controller/post.controller.ts +++ b/src/v2/controller/post.controller.ts @@ -13,15 +13,15 @@ import { Body, Put, Delete, - Query, } from "tsoa"; import DOMPurify from "isomorphic-dompurify"; import { WriteConfirmation } from "firestorm-db"; import { - WebsitePostDownloadRecord, - WebsitePostChangelogRecord, + PostDownload, + PostChangelog, WebsitePost, CreateWebsitePost, + WebsitePosts, } from "../interfaces"; import { BadRequestError, NotFoundError, PermissionError } from "../tools/errors"; @@ -40,46 +40,23 @@ export class PostController extends Controller { @Response(404) @Security("discord", ["administrator"]) @Security("bot") - @Get("/raw") + @Get("raw") public getRaw(): Promise> { return this.service.getRaw(); } /** - * Get all the published posts + * Get any add-on by ID, status, or slug (needs to be authenticated for non-approved add-on) + * Note: slugs with slashes need to be escaped (/ -> %2F) + * @param id_or_slug Desired post slug + * @example Slug "/faithful64x/B4" */ @Response(404) - @Get("/") - public getAll(): Promise> { - return this.service.getRaw().then((r) => - Object.keys(r).reduce((acc, cur) => { - if (r[cur].published) acc[cur] = r[cur]; - return acc; - }, {}), - ); - } - - /** - * Get any post with permalink - * @param permalink Desired post permalink - * @example Permalink "/faithful64x/B4" - */ - @Response(404) - @Get("bypermalink") - public getPostByPermalink(@Query() permalink: string): Promise { - return cache.handle(`website-post-${encodeURI(permalink)}`, () => - this.service.getByPermalink(permalink), - ); - } - - /** - * Get any post by ID - * @param id Desired post ID - */ - @Response(404) - @Get("{id}") - public getPostById(@Path() id: number): Promise { - return cache.handle(`website-post-${id}`, () => this.service.getById(id)); + @Security("discord", ["post:approved", "administrator"]) + @Get("{id_or_slug}") + public getPostByPermalink(@Path() id_or_slug: string): Promise { + if (id_or_slug === "approved") return this.service.getApprovedPosts(); + return this.service.getByIdOrPermalink(id_or_slug); } /** @@ -102,7 +79,7 @@ export class PostController extends Controller { */ @Response(404) @Get("{id}/downloads") - public getPostDownloads(@Path() id: number): Promise { + public getPostDownloads(@Path() id: number): Promise { return cache.handle(`website-post-downloads-${id}`, () => this.service.getDownloadsForId(id)); } @@ -112,7 +89,7 @@ export class PostController extends Controller { */ @Response(404) @Get("{id}/changelog") - public getPostChangelog(@Path() id: number): Promise { + public getPostChangelog(@Path() id: number): Promise { return this.service.getChangelogForId(id); } diff --git a/src/v2/interfaces/posts.ts b/src/v2/interfaces/posts.ts index 6bb452e..0a9bba4 100644 --- a/src/v2/interfaces/posts.ts +++ b/src/v2/interfaces/posts.ts @@ -1,20 +1,13 @@ import { WriteConfirmation } from "firestorm-db"; -export interface Post { - id: string; // post unique id - name: string; // post name (> 5 && < 30) - description: string; // post description (> 256 && < 4096) - authors: string[]; // discord users IDs - slug: string; // used in link (ex: 'www.faithfulpack.net/faithful32x/R2') -} -export type Posts = Post[]; +export type PostDownload = + | Record> // each category: names -> links + | Record; // just names -> links -interface WebsitePostDownload { - name: string; // download label - url: string; // download URL +export interface PostChangelog { + // recursive type so we need an interface (idk why either blame typescript) + [key: string]: PostChangelog | string; } -export type WebsitePostDownloadRecord = Record; -export type WebsitePostChangelogRecord = Record>; export interface CreateWebsitePost { title: string; // Post main title @@ -23,18 +16,21 @@ export interface CreateWebsitePost { header_img?: string; // header image url description: string; // post HTML content published: boolean; - downloads?: WebsitePostDownloadRecord; // possible downloads attached - changelog?: WebsitePostChangelogRecord; // possible article changelog attached + downloads?: PostDownload; // attached downloads + changelog?: PostChangelog; // attached article changelog } export interface WebsitePost extends CreateWebsitePost { id: string; } +export type WebsitePosts = WebsitePost[]; + export interface FirestormPost extends WebsitePost {} export interface WebsitePostRepository { getRaw(): Promise>; + getApproved(): Promise; getById(id: number): Promise; getByPermalink(permalink: string): Promise; create(post: CreateWebsitePost): Promise; diff --git a/src/v2/repository/posts.repository.ts b/src/v2/repository/posts.repository.ts index a4dec36..9613b01 100644 --- a/src/v2/repository/posts.repository.ts +++ b/src/v2/repository/posts.repository.ts @@ -1,12 +1,22 @@ import { ID_FIELD, WriteConfirmation } from "firestorm-db"; import { posts } from "../firestorm"; -import { CreateWebsitePost, WebsitePost, WebsitePostRepository } from "../interfaces"; +import { CreateWebsitePost, WebsitePost, WebsitePostRepository, WebsitePosts } from "../interfaces"; export default class PostFirestormRepository implements WebsitePostRepository { getRaw(): Promise> { return posts.readRaw(); } + getApproved(): Promise { + return posts.search([ + { + field: "published", + criteria: "==", + value: true, + }, + ]); + } + getById(id: number): Promise { return posts.get(id); } @@ -25,10 +35,7 @@ export default class PostFirestormRepository implements WebsitePostRepository { create(postToCreate: CreateWebsitePost): Promise { const { permalink } = postToCreate; - return posts - .add(postToCreate) - .then(() => this.getByPermalink(permalink)) - .then((results) => results[0]); + return posts.add(postToCreate).then(() => this.getByPermalink(permalink)); } update(id: number, post: CreateWebsitePost): Promise { @@ -40,6 +47,6 @@ export default class PostFirestormRepository implements WebsitePostRepository { } delete(id: number): Promise { - return posts.remove(String(id)); + return posts.remove(id); } } diff --git a/src/v2/service/post.service.ts b/src/v2/service/post.service.ts index 22b8cb6..289d7c6 100644 --- a/src/v2/service/post.service.ts +++ b/src/v2/service/post.service.ts @@ -1,9 +1,10 @@ import { WriteConfirmation } from "firestorm-db"; import { - WebsitePostDownloadRecord, - WebsitePostChangelogRecord, + PostDownload, + PostChangelog, WebsitePost, CreateWebsitePost, + WebsitePosts, } from "../interfaces"; import { NotFoundError } from "../tools/errors"; import PostFirestormRepository from "../repository/posts.repository"; @@ -11,14 +12,20 @@ import PostFirestormRepository from "../repository/posts.repository"; export default class PostService { private readonly postRepo = new PostFirestormRepository(); - public async getByIdOrPermalink(idOrPermalink: string): Promise { + getByPermalink(permalink: string): Promise { + return this.postRepo + .getByPermalink(permalink) + .catch(() => Promise.reject(new NotFoundError("Post not found"))); + } + + public async getByIdOrPermalink(idOrSlug: string): Promise { let postFound: WebsitePost | undefined; - const parsed = Number(idOrPermalink); + const parsed = Number(idOrSlug); if (!Number.isNaN(parsed)) postFound = await this.getById(parsed).catch(() => undefined); if (postFound === undefined) - postFound = await this.getByPermalink(idOrPermalink).catch(() => undefined); + postFound = await this.getByPermalink(idOrSlug).catch(() => undefined); if (postFound !== undefined) return postFound; @@ -35,18 +42,16 @@ export default class PostService { .catch(() => Promise.reject(new NotFoundError("Post not found"))); } - getByPermalink(permalink: string): Promise { - return this.postRepo - .getByPermalink(permalink) - .catch(() => Promise.reject(new NotFoundError("Post not found"))); + getApprovedPosts(): Promise { + return this.postRepo.getApproved(); } - async getDownloadsForId(id: number): Promise { + async getDownloadsForId(id: number): Promise { const post = await this.getById(id); return post.downloads || null; } - async getChangelogForId(id: number): Promise { + async getChangelogForId(id: number): Promise { const post = await this.getById(id); return post.changelog || null; } diff --git a/src/v2/tools/authentication.ts b/src/v2/tools/authentication.ts index 1da3842..e0342d9 100644 --- a/src/v2/tools/authentication.ts +++ b/src/v2/tools/authentication.ts @@ -6,11 +6,13 @@ import { APIUser } from "discord-api-types/v10"; import { PermissionError, NotFoundError, APIError } from "./errors"; import UserService from "../service/user.service"; import AddonService from "../service/addon.service"; +import PostService from "../service/post.service"; import { Addon } from "../interfaces"; import { AddonStatusApproved, AddonStatusValues } from "../interfaces/addons"; const userService = new UserService(); const addonService = new AddonService(); +const postService = new PostService(); const isSlug = (idOrSlug: string): boolean => !AddonStatusValues.includes(idOrSlug as any); function getRequestKey({ headers, query }: ExRequest, key: string, queryAllowed = false): string { @@ -28,15 +30,22 @@ export async function expressAuthentication( ): Promise { scopes ||= []; - // handle public add-ons without a token (for website etc) - if (scopes.includes("addon:approved") && "id_or_slug" in request.params) { - // /v2/addons/approved is public, safe to send - if (request.params.id_or_slug === AddonStatusApproved) return true; + // handle public add-ons/posts without a token (for website etc) + if ("id_or_slug" in request.params) { + if (scopes.includes("addon:approved")) { + // /v2/addons/approved is public, safe to send + if (request.params.id_or_slug === AddonStatusApproved) return true; - // it's an addon slug and not a status - if (isSlug(request.params.id_or_slug)) { - const addon = (await addonService.getAddonFromSlugOrId(request.params.id_or_slug))[1]; - if (addon.approval.status === AddonStatusApproved) return true; + // it's an addon slug and not a status + if (isSlug(request.params.id_or_slug)) { + const addon = (await addonService.getAddonFromSlugOrId(request.params.id_or_slug))[1]; + if (addon.approval.status === AddonStatusApproved) return true; + } + } + if (scopes.includes("post:approved")) { + if (request.params.id_or_slug === "approved") return true; + const post = await postService.getByIdOrPermalink(request.params.id_or_slug); + if (post.published === true) return true; } }