Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9a43270
feat(vscode): Add option to rebuild gRPC bindings
ashquarky May 16, 2025
d6a5d2d
feat(grpc): Move headers into the JSON object
ashquarky May 16, 2025
f2b6938
fix(grpc): Log the error object
ashquarky May 16, 2025
a4da331
feat(grpc): helpers for responding with error codes
ashquarky May 16, 2025
93e3ad1
feat(grpc): Work-in-progress authentication middleware
ashquarky May 16, 2025
a4f5153
chore(internal): Refactor error handling
ashquarky May 16, 2025
c695ef2
feat(internal): Implement authentication and tweak error handling more
ashquarky May 16, 2025
d9f70e9
feat(internal): Check for banned accounts
ashquarky May 16, 2025
06ab562
feat(internal): WIP posts endpoint (auth not working)
ashquarky May 16, 2025
2be4de2
fix(ui): Don't crash on promise rejection (please)
ashquarky May 24, 2025
fc8204c
fix(internal): enable posts route
ashquarky May 24, 2025
5c91b11
fix(server): Fix incorrect Express error handler
ashquarky Jun 8, 2025
952c758
feat(internal): Validate and use posts endpoint, tweak tokens handling
ashquarky Jun 8, 2025
771b649
feat(internal): Set up guest access controls per-endpoint
ashquarky Jun 8, 2025
126378b
fix(server): Force re-login if backend rejects user token
ashquarky Jun 8, 2025
9668d68
feat(ui): Support redirecting out of the login page
ashquarky Jun 8, 2025
6d8eced
fix(login): Validate redirect path
ashquarky Jun 10, 2025
70907d2
fix(api): Audit and fix schema to reflect real db
ashquarky Jun 11, 2025
83221c9
fix(login): Validation for the validation gods
ashquarky Jun 11, 2025
4b8ca00
feat(api): Rate-limiting for the ratelimiting gods
ashquarky Jun 11, 2025
299db63
feat(internal): Introduce dto contract type
ashquarky Jun 12, 2025
586e77d
feat(internal): Add explicit nulls for 404s
ashquarky Jun 12, 2025
77d331c
fix(internal): Restructure auth and guard middlewares
ashquarky Jun 12, 2025
5716ddd
Merge branch 'dev' into work/grpc
ashquarky Jun 12, 2025
eda12fb
fix(api): Tweak ratelimits
ashquarky Jun 13, 2025
e81f230
feat(internal): Stronger checks on account status + mod mode
ashquarky Jun 13, 2025
1e1f0bf
feat(internal): Show moderators removed posts
ashquarky Jun 13, 2025
dad0b14
fix(web): Provide redirect on all paths
ashquarky Jun 13, 2025
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
65 changes: 65 additions & 0 deletions apps/juxtaposition-ui/src/api/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { apiFetchUser } from '@/fetch';
import type { UserTokens } from '@/fetch';

/* !!! HEY
* This type lives in apps/miiverse-api/src/services/internal/contract/post.ts
* Modify it there and copy-paste here! */

/* This type is the contract for the frontend. If we make changes to the db, this shape should be kept. */
export type PostDto = {
id: string;
title_id?: string; // number
screen_name: string;
body: string;
app_data?: string; // nintendo base64

painting?: string; // base64 or '', undef for PMs
screenshot?: string; // URL frag (leading /) or '', undef for PMs
screenshot_length?: number;

search_key?: string[]; // can be []
topic_tag?: string; // can be ''

community_id: string; // number
created_at: string; // ISO Z
feeling_id?: number;

is_autopost: boolean;
is_community_private_autopost: boolean;
is_spoiler: boolean;
is_app_jumpable: boolean;

empathy_count: number;
country_id: number;
language_id: number;

mii: string; // nintendo base64
mii_face_url: string; // full URL (cdn., r2-cdn.)

pid: number;
platform_id?: number;
region_id?: number;
parent: string | null;

reply_count: number;
verified: boolean;

message_to_pid: string | null;
removed: boolean;
removed_by?: number;
removed_at?: string; // ISO Z
removed_reason?: string;

yeahs: number[];
};

/**
* Fetches a Post for a given ID.
* @param tokens User to perform fetch as. Responses will be according to this users' permissions (user, moderator etc.)
* @param post_id The ID of the post to get.
* @returns Post object
*/
export async function getPostById(tokens: UserTokens, post_id: string): Promise<PostDto | null> {
const post = await apiFetchUser<PostDto>(tokens, `/api/v1/posts/${post_id}`);
return post;
}
10 changes: 0 additions & 10 deletions apps/juxtaposition-ui/src/backend.ts

This file was deleted.

5 changes: 4 additions & 1 deletion apps/juxtaposition-ui/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const schema = z.object({
logSensitive: zodCoercedBoolean().default(false),
httpPort: z.coerce.number().default(8080),
httpCookieDomain: z.string().default('.pretendo.network'),
/** "Safe" base origin for login-redirect validation */
httpBaseUrl: z.string().default('https://juxt.pretendo.network'),
/** Configures proxy trust (X-Forwarded-For etc.). Can be `true` to unconditionally trust, or
* provide a numeric hop count, or comma-seperated CIDR ranges.
* See https://expressjs.com/en/guide/behind-proxies.html
Expand Down Expand Up @@ -94,7 +96,8 @@ export const config = {
http: {
port: unmappedConfig.httpPort,
cookieDomain: unmappedConfig.httpCookieDomain,
trustProxy: unmappedConfig.httpTrustProxy
trustProxy: unmappedConfig.httpTrustProxy,
baseUrl: unmappedConfig.httpBaseUrl
},
metrics: {
enabled: unmappedConfig.metricsEnabled,
Expand Down
8 changes: 5 additions & 3 deletions apps/juxtaposition-ui/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function isErrorHttpStatus(status: number): boolean {
return status >= 400 && status < 600;
}

export async function apiFetch<T>(path: string, options?: FetchOptions): Promise<T> {
export async function apiFetch<T>(path: string, options?: FetchOptions): Promise<T | null> {
const defaultedOptions = {
method: 'GET',
headers: {},
Expand All @@ -45,7 +45,9 @@ export async function apiFetch<T>(path: string, options?: FetchOptions): Promise
metadata
});

if (isErrorHttpStatus(response.status)) {
if (response.status === 404) {
return null;
} else if (isErrorHttpStatus(response.status)) {
throw FetchError(response, `HTTP error! status: ${response.status} ${response.payload}`);
}

Expand All @@ -57,7 +59,7 @@ export type UserTokens = {
oauthToken?: string;
};

export async function apiFetchUser<T>(tokens: UserTokens, path: string, options?: FetchOptions): Promise<T> {
export async function apiFetchUser<T>(tokens: UserTokens, path: string, options?: FetchOptions): Promise<T | null> {
options = {
...options,
headers: {
Expand Down
61 changes: 0 additions & 61 deletions apps/juxtaposition-ui/src/models/api/post.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const upload = multer({ dest: 'uploads/' });
const redis = require('@/redisCache');
const { config } = require('@/config');
const router = express.Router();
const backend = require('@/backend');
const { getPostById } = require('@/api/post');

const postLimit = rateLimit({
windowMs: 15 * 1000, // 30 seconds
Expand Down Expand Up @@ -42,7 +42,10 @@ const yeahLimit = rateLimit({
});

router.get('/:post_id/oembed.json', async function (req, res) {
const post = await backend.getPostById(req.tokens, req.params.post_id);
const post = await getPostById(req.tokens, req.params.post_id);
if (!post) {
return res.sendStatus(404);
}
const doc = {
author_name: post.screen_name,
author_url: 'https://juxt.pretendo.network/users/show?pid=' + post.pid
Expand Down Expand Up @@ -110,9 +113,15 @@ router.get('/:post_id', async function (req, res) {
const userSettings = await database.getUserSettings(req.pid);
const userContent = await database.getUserContent(req.pid);

const post = await backend.getPostById(req.tokens, req.params.post_id);
const post = await getPostById(req.tokens, req.params.post_id);
if (!post) {
return res.redirect('/404');
}
if (post.parent) {
const parent = await backend.getPostById(req.tokens, post.parent);
const parent = await getPostById(req.tokens, post.parent);
if (!parent) {
return res.redirect('/404');
}
return res.redirect(`/posts/${parent.id}`);
}
const community = await database.getCommunityByID(post.community_id);
Expand Down Expand Up @@ -161,7 +170,7 @@ router.post('/:post_id/new', postLimit, upload.none(), async function (req, res)

router.post('/:post_id/report', upload.none(), async function (req, res) {
const { reason, message, post_id } = req.body;
const post = await backend.getPostByID(req.tokens, post_id);
const post = await getPostById(req.tokens, post_id);
if (!reason || !post_id || !post) {
return res.redirect('/404');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ router.post('/', async (req, res) => {
res.cookie('access_token', login.accessToken, { domain: cookieDomain, maxAge: expiration });
res.cookie('refresh_token', login.refreshToken, { domain: cookieDomain });
res.cookie('token_type', 'Bearer', { domain: cookieDomain });
res.redirect(redirect);

/* Only allow relative URLs (leading /) or absolute ones on config.http.baseUrl. */
const safe_redirect = new URL(redirect, config.http.baseUrl).origin == config.http.baseUrl ? redirect : '/';

res.redirect(safe_redirect);
});

module.exports = router;
3 changes: 2 additions & 1 deletion apps/miiverse-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"express-prom-bundle": "^7.0.2",
"express-rate-limit": "^7.5.0",
"express-subdomain": "^1.0.5",
"fs-extra": "^9.0.0",
"moment": "^2.24.0",
"mongoose": "^6.10.1",
"mongoose": "^8.15.1",
"multer": "^1.4.5-lts.1",
"nice-grpc": "^2.1.12",
"node-snowflake": "0.0.1",
Expand Down
4 changes: 2 additions & 2 deletions apps/miiverse-api/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { HydratedConversationDocument } from '@/types/mongoose/conversation
import type { HydratedContentDocument } from '@/types/mongoose/content';
import type { HydratedSettingsDocument } from '@/types/mongoose/settings';
import type { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
import type { HydratedPostDocument, IPost } from '@/types/mongoose/post';
import type { HydratedPostDocument, IPostInput } from '@/types/mongoose/post';
import type { HydratedCommunityDocument } from '@/types/mongoose/community';

let connection: mongoose.Connection;
Expand Down Expand Up @@ -97,7 +97,7 @@ export async function getPostReplies(postID: string, limit: number): Promise<Hyd
}).limit(limit);
}

export async function getDuplicatePosts(pid: number, post: IPost): Promise<HydratedPostDocument | null> {
export async function getDuplicatePosts(pid: number, post: IPostInput): Promise<HydratedPostDocument | null> {
verifyConnected();

return Post.findOne({
Expand Down
Loading