Skip to content

Commit

Permalink
Proper cors handling; Extend Guild details page
Browse files Browse the repository at this point in the history
  • Loading branch information
l7ssha committed Dec 17, 2024
1 parent 7d80f4f commit 7814120
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 25 deletions.
17 changes: 15 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"react-scripts": "5.0.1",
"styled-components": "^6.1.13",
"typescript": "^4.4.2",
"use-debounce": "^10.0.4",
"web-vitals": "^2.1.0"
},
"scripts": {
Expand Down
27 changes: 20 additions & 7 deletions frontend/src/page/admin/GuildDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {fetchGuildDetails, GuildDetails as GuildDetailsDto} from "../../service/api";
import {fetchGuildDetails, fetchGuildTags, GuildDetails as GuildDetailsDto} from "../../service/api";
import {Base} from "../../component/Base";
import {
Accordion,
Expand All @@ -11,12 +11,13 @@ import {
Typography
} from "@mui/material";
import {useParams} from "react-router-dom";
import React, {Suspense, use} from "react";
import React, {Suspense, use, useEffect, useState} from "react";
import {getGuildNameElement} from "../../guildUtil";
import {DataGrid, GridColDef} from "@mui/x-data-grid";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {parseISO} from "date-fns";
import {format} from "date-fns/format";
import {useDebounce} from "use-debounce";

interface GuildDetailsDataProps {
dataPromise: Promise<GuildDetailsDto>
Expand All @@ -33,13 +34,13 @@ function FeaturesPaper({dataPromise}: GuildDetailsDataProps) {
{f.name} (enabled: {enabledAt})
</AccordionSummary>
<AccordionDetails>
<Typography><Typography fontWeight="bold" display="inline">Enabled by: </Typography> {f.enabledBy}</Typography>
<Typography><Typography fontWeight="bold" display="inline">Data: </Typography> {JSON.stringify(f.data)}</Typography>
<Typography fontWeight="bold" display="inline">Enabled by: </Typography><Typography display="inline">{f.enabledBy}</Typography>
<Typography fontWeight="bold">Data: </Typography><Typography>{JSON.stringify(f.data)}</Typography>
</AccordionDetails>
</Accordion>;
});

return <Stack direction="column">
return <Stack direction="column">
<Typography variant='h5'>Features</Typography>
<div>
{enabledFeatures}
Expand All @@ -50,6 +51,18 @@ function FeaturesPaper({dataPromise}: GuildDetailsDataProps) {
function TagsDataPaper({dataPromise}: GuildDetailsDataProps) {
const data = use(dataPromise);

const [tags, setTags] = useState(data.tags);
const [searchQuery, setSearchQuery] = useState<string|null>(null);
const [searchQueryDebounced] = useDebounce(searchQuery, 500);

useEffect(() => {
if (searchQueryDebounced == null) {
return;
}

fetchGuildTags({id: data.id, query: searchQueryDebounced}).then((t) => setTags(t));
}, [searchQueryDebounced]);

const columns: GridColDef[] = [
{ field: 'name', headerName: 'Name' },
{ field: 'content', headerName: 'Content', flex: 1 },
Expand All @@ -60,9 +73,9 @@ function TagsDataPaper({dataPromise}: GuildDetailsDataProps) {
return <Stack direction="column">
<Stack direction='row' spacing={{sm: 5}} sx={{p: '5px'}}>
<Typography variant='h5'>Tags</Typography>
<TextField id="tag-name-filter" label="Name..." variant="outlined" size='small' />
<TextField id="tag-name-filter" label="Name..." variant="outlined" size='small' onChange={(e) => setSearchQuery(e.target.value)} />
</Stack>
<DataGrid rows={data.tags} columns={columns}/>
<DataGrid rows={tags} columns={columns} />
</Stack>;
}

Expand Down
15 changes: 15 additions & 0 deletions frontend/src/service/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,18 @@ export async function fetchGuilds(): Promise<GuildSummary[]> {
export async function fetchGuildDetails(id: string): Promise<GuildDetails> {
return await request<GuildDetails>({path: `/api/guilds/${id}`, auth: true});
}

interface FetchGuildTags {
id: string,
perPage?: number,
query?: string,
}

export async function fetchGuildTags({id, perPage = 5, query}: FetchGuildTags): Promise<Tag[]> {
const params = [["perPage", perPage.toString()]];
if (query != null && query !== '') {
params.push(["query", query])
}

return await request<Tag[]>({path: `/api/guilds/${id}/tags`, auth: true, searchParams: params});
}
12 changes: 9 additions & 3 deletions frontend/src/service/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@ export interface FetchParams {
path: string,
method?: string,
auth?: boolean
searchParams?: string|string[][]|URLSearchParams|Record<string, string>
}

interface ErrorResponse {
message: string
}

export async function request<T extends object>({path, method = 'GET', auth = false}: FetchParams) {
export async function request<T extends object>({path, method = 'GET', auth = false, searchParams}: FetchParams) {
const headers = new Headers();
headers.append('Accept', 'application/json');

if (auth) {
headers.append('authorization', `Bearer ${getToken()}`)
}

const url = new URL(`${API_SERVER}${path}`);
if (searchParams != undefined) {
url.search = new URLSearchParams(searchParams).toString();
}

const response = await fetch(
`${API_SERVER}${path}`,
url,
{
method: method,
headers: headers,
Expand All @@ -33,7 +39,7 @@ export async function request<T extends object>({path, method = 'GET', auth = fa
return responseBody as T;
}

if ([400, 401].includes(response.status)) {
if ([401, 403].includes(response.status)) {
logout();
}

Expand Down
3 changes: 3 additions & 0 deletions lib/src/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ final int webServerPort = getEnvInt('WEB_SERVER_PORT', 8088);
/// Path to templates directory
final String webServerTemplatesDirectory = getEnv('WEB_SERVER_TEMPLATES_DIRECTORY', "./templates");

/// Allowed origins for cors headers
final String webServerAllowedOrigins = getEnv('WEB_SERVER_ALLOWED_ORIGIN', 'rod.l7ssha.xyz');

/// The GitHub account to use when no other account is specified.
final String githubAccount = getEnv('ROD_GITHUB_ACCOUNT', 'nyxx-discord');

Expand Down
35 changes: 24 additions & 11 deletions lib/src/web_app/api_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:nyxx/nyxx.dart';
import 'package:running_on_dart/running_on_dart.dart';
import 'package:running_on_dart/src/web_app/jwt.dart';
import 'package:running_on_dart/src/web_app/mapper/guild_mapper.dart';
import 'package:running_on_dart/src/web_app/mapper/tags_mapper.dart';
import 'package:running_on_dart/src/web_app/utils.dart';
import 'package:running_on_dart/src/services/bot_info.dart';
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
Expand All @@ -34,6 +35,20 @@ class WebServer {
return createOkResponse(guildData);
}

Future<shelf.Response> _handleGuildTags(shelf.Request request) async {
final guildParam = request.params['id'];
if (guildParam == null) {
return createBadRequestResponse("Missing id param");
}

final searchQuery = request.requestedUri.queryParameters['query'];
final tagsLimit = int.tryParse(request.requestedUri.queryParameters['perPage'] ?? '5') ?? 5;

return createOkResponse(
await mapGuildTagsToData(Snowflake.parse(guildParam), tagsLimit, searchQuery: searchQuery).toList(),
);
}

Future<shelf.Response> _handleGuildDetails(shelf.Request request) async {
final client = Injector.appInstance.get<NyxxGateway>();

Expand All @@ -43,20 +58,13 @@ class WebServer {
}

final channelsLimit = int.tryParse(request.requestedUri.queryParameters['channelsLimit'] ?? '0') ?? 0;
final rolesLimit = int.tryParse(request.requestedUri.queryParameters['rolesLimit'] ?? '0')?? 0;
final rolesLimit = int.tryParse(request.requestedUri.queryParameters['rolesLimit'] ?? '0') ?? 0;
final tagsLimit = int.tryParse(request.requestedUri.queryParameters['tagsLimit'] ?? '5') ?? 5;

try {
final guild = await client.guilds.get(Snowflake.parse(guildParam));

return createOkResponse(
await mapGuildToDetailsData(
guild,
channelsLimit,
rolesLimit,
tagsLimit
)
);
return createOkResponse(await mapGuildToDetailsData(guild, channelsLimit, rolesLimit, tagsLimit));
} on HttpResponseError {
return createNotFoundResponse();
}
Expand Down Expand Up @@ -126,6 +134,7 @@ class WebServer {
..get("/api/guilds", _requireJwt(_handleGuilds, [JwtPermission.guilds]))
// ..get("/api/guilds/<id>", _requireJwt(_handleGuildDetails, [JwtPermission.guilds]))
..get("/api/guilds/<id>", _handleGuildDetails)
..get("/api/guilds/<id>/tags", _handleGuildTags)
..get("/api/validate-oauth", _handleValidateCode)
..all(r"/<ignored|.+\w+\.\w+$>", staticHandler)
..all("/<ignored|.*>", _handleIndex);
Expand All @@ -142,8 +151,12 @@ class WebServer {

final router = await _setupRouter();

final app =
const shelf.Pipeline().addMiddleware(shelf.logRequests()).addMiddleware(corsHeaders()).addHandler(router.call);
final corsChecker = dev ? originAllowAll : originOneOf(webServerAllowedOrigins.split(','));

final app = const shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addMiddleware(corsHeaders(originChecker: corsChecker))
.addHandler(router.call);

_logger.info("Starting server at: http://$webServerHost:$webServerPort/");
await shelf_io.serve(app, webServerHost, webServerPort);
Expand Down
9 changes: 7 additions & 2 deletions lib/src/web_app/mapper/tags_mapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import 'package:nyxx/nyxx.dart';
import 'package:running_on_dart/src/modules/tag.dart';
import 'package:running_on_dart/src/web_app/utils.dart';

Stream<JsonApiResponse> mapGuildTagsToData(Snowflake guildId, int tagsLimit) async* {
Stream<JsonApiResponse> mapGuildTagsToData(Snowflake guildId, int tagsLimit, {String? searchQuery}) async* {
final tagsModule = Injector.appInstance.get<TagModule>();

for (final tag in tagsModule.getGuildTags(guildId).take(tagsLimit)) {
var tags = tagsModule.getGuildTags(guildId);
if (searchQuery != null) {
tags = tags.where((tag) => tag.name.contains(searchQuery));
}

for (final tag in tags.take(tagsLimit)) {
yield {
'id': tag.id,
'name': tag.name,
Expand Down

0 comments on commit 7814120

Please sign in to comment.