Skip to content

Commit

Permalink
overhaul post backend to match addons more
Browse files Browse the repository at this point in the history
  • Loading branch information
3vorp committed Oct 7, 2024
1 parent b7d769a commit 58382cc
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 78 deletions.
53 changes: 15 additions & 38 deletions src/v2/controller/post.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -40,46 +40,23 @@ export class PostController extends Controller {
@Response<NotFoundError>(404)
@Security("discord", ["administrator"])
@Security("bot")
@Get("/raw")
@Get("raw")
public getRaw(): Promise<Record<string, WebsitePost>> {
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<NotFoundError>(404)
@Get("/")
public getAll(): Promise<Record<string, WebsitePost>> {
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<NotFoundError>(404)
@Get("bypermalink")
public getPostByPermalink(@Query() permalink: string): Promise<WebsitePost> {
return cache.handle(`website-post-${encodeURI(permalink)}`, () =>
this.service.getByPermalink(permalink),
);
}

/**
* Get any post by ID
* @param id Desired post ID
*/
@Response<NotFoundError>(404)
@Get("{id}")
public getPostById(@Path() id: number): Promise<WebsitePost> {
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<WebsitePost | WebsitePosts> {
if (id_or_slug === "approved") return this.service.getApprovedPosts();
return this.service.getByIdOrPermalink(id_or_slug);
}

/**
Expand All @@ -102,7 +79,7 @@ export class PostController extends Controller {
*/
@Response<NotFoundError>(404)
@Get("{id}/downloads")
public getPostDownloads(@Path() id: number): Promise<WebsitePostDownloadRecord | null> {
public getPostDownloads(@Path() id: number): Promise<PostDownload | null> {
return cache.handle(`website-post-downloads-${id}`, () => this.service.getDownloadsForId(id));
}
Expand All @@ -112,7 +89,7 @@ export class PostController extends Controller {
*/
@Response<NotFoundError>(404)
@Get("{id}/changelog")
public getPostChangelog(@Path() id: number): Promise<WebsitePostChangelogRecord | null> {
public getPostChangelog(@Path() id: number): Promise<PostChangelog | null> {
return this.service.getChangelogForId(id);
}
Expand Down
26 changes: 11 additions & 15 deletions src/v2/interfaces/posts.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, string>> // each category: names -> links
| Record<string, string>; // 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<string, WebsitePostDownload[]>;
export type WebsitePostChangelogRecord = Record<string, Record<string, string[]>>;

export interface CreateWebsitePost {
title: string; // Post main title
Expand All @@ -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<Record<string, WebsitePost>>;
getApproved(): Promise<WebsitePost[]>;
getById(id: number): Promise<WebsitePost>;
getByPermalink(permalink: string): Promise<WebsitePost>;
create(post: CreateWebsitePost): Promise<WebsitePost>;
Expand Down
19 changes: 13 additions & 6 deletions src/v2/repository/posts.repository.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, WebsitePost>> {
return posts.readRaw();
}

getApproved(): Promise<WebsitePosts> {
return posts.search([
{
field: "published",
criteria: "==",
value: true,
},
]);
}

getById(id: number): Promise<WebsitePost> {
return posts.get(id);
}
Expand All @@ -25,10 +35,7 @@ export default class PostFirestormRepository implements WebsitePostRepository {

create(postToCreate: CreateWebsitePost): Promise<WebsitePost> {
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<WebsitePost> {
Expand All @@ -40,6 +47,6 @@ export default class PostFirestormRepository implements WebsitePostRepository {
}

delete(id: number): Promise<WriteConfirmation> {
return posts.remove(String(id));
return posts.remove(id);
}
}
27 changes: 16 additions & 11 deletions src/v2/service/post.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
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";

export default class PostService {
private readonly postRepo = new PostFirestormRepository();

public async getByIdOrPermalink(idOrPermalink: string): Promise<WebsitePost> {
getByPermalink(permalink: string): Promise<WebsitePost> {
return this.postRepo
.getByPermalink(permalink)
.catch(() => Promise.reject(new NotFoundError("Post not found")));
}

public async getByIdOrPermalink(idOrSlug: string): Promise<WebsitePost> {
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;

Expand All @@ -35,18 +42,16 @@ export default class PostService {
.catch(() => Promise.reject(new NotFoundError("Post not found")));
}

getByPermalink(permalink: string): Promise<WebsitePost> {
return this.postRepo
.getByPermalink(permalink)
.catch(() => Promise.reject(new NotFoundError("Post not found")));
getApprovedPosts(): Promise<WebsitePosts> {
return this.postRepo.getApproved();
}

async getDownloadsForId(id: number): Promise<WebsitePostDownloadRecord | null> {
async getDownloadsForId(id: number): Promise<PostDownload | null> {
const post = await this.getById(id);
return post.downloads || null;
}

async getChangelogForId(id: number): Promise<WebsitePostChangelogRecord | null> {
async getChangelogForId(id: number): Promise<PostChangelog | null> {
const post = await this.getById(id);
return post.changelog || null;
}
Expand Down
25 changes: 17 additions & 8 deletions src/v2/tools/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,15 +30,22 @@ export async function expressAuthentication(
): Promise<any> {
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;
}
}

Expand Down

0 comments on commit 58382cc

Please sign in to comment.