Compare commits

...

15 Commits

Author SHA1 Message Date
ic3w0lf
3f848a35c8 implement cache de-exister 2025-06-05 17:42:34 -06:00
ic3w0lf
8d5bd9e523 Fix error & include error message in response headers 2025-06-03 20:52:43 -06:00
ic3w0lf
e1fc637619 Implement errorImageResponse 2025-06-03 20:42:37 -06:00
ic3w0lf
762ee874a0 thumbnail fix - will this WORK THIS TIME? 2025-06-03 20:03:09 -06:00
a1c84ff225 Merge pull request 'web: fix api middleware' (#153) from pr1 into staging
Reviewed-on: StrafesNET/maps-service#153
2025-06-04 01:45:36 +00:00
cea6242dd7 web: fix api middleware 2025-06-03 18:42:21 -07:00
fefe116611 Merge pull request 'Allow Submitter Comments' (#151) from submitter-can-comment into staging
Reviewed-on: StrafesNET/maps-service#151
2025-06-04 00:19:35 +00:00
0ada77421f fix bug 2025-06-03 17:17:59 -07:00
fa2d611534 submissions: allow submitter special permission to comment on their posts
Previously only map council could comment.
2025-06-03 17:11:53 -07:00
81539a606c Merge pull request 'API_HOST changes, thumbnail fix & cache, "list is empty" fix' (#150) from thumbnail-fix into staging
Reviewed-on: StrafesNET/maps-service#150
Reviewed-by: Quaternions <quaternions@noreply@itzana.me>
2025-06-03 23:54:13 +00:00
32095296c2 Merge branch 'staging' into thumbnail-fix 2025-06-03 23:53:55 +00:00
8ea5ee2d41 use null instead of sentinel value 2025-06-03 16:29:29 -07:00
954dbaeac6 env var name change requires deployment configuration change 2025-06-03 16:27:42 -07:00
5b7efa2426 Merge pull request 'Add a favicon (#141)' (#149) from aidan9382/maps-service:favicon into staging
Reviewed-on: StrafesNET/maps-service#149
Reviewed-by: Quaternions <quaternions@noreply@itzana.me>
2025-06-03 23:16:38 +00:00
4f31f8c75a Add a favicon (#141) 2025-06-03 22:32:43 +01:00
11 changed files with 156 additions and 91 deletions

View File

@@ -26,10 +26,10 @@ Prerequisite: golang installed
Prerequisite: bun installed
The environment variables `BASE_URL` and `AUTH_HOST` will need to be set for the middleware.
The environment variables `API_HOST` and `AUTH_HOST` will need to be set for the middleware.
Example `.env` in web's root:
```
BASE_URL="http://localhost:8082/"
API_HOST="http://localhost:8082/"
AUTH_HOST="http://localhost:8083/"
```

View File

@@ -25,15 +25,24 @@ func (svc *Service) CreateMapfixAuditComment(ctx context.Context, req api.Create
if err != nil {
return err
}
if !has_role {
return ErrPermissionDeniedNeedRoleMapfixReview
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
if !has_role {
// Submitter has special permission to comment on their mapfix
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
if err != nil {
return err
}
if mapfix.Submitter != userId {
return ErrPermissionDeniedNeedRoleMapfixReview
}
}
data := []byte{}
_, err = req.Read(data)
if err != nil {
@@ -146,15 +155,24 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr
if err != nil {
return err
}
if !has_role {
return ErrPermissionDeniedNeedRoleSubmissionReview
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
if !has_role {
// Submitter has special permission to comment on their submission
submission, err := svc.DB.Submissions().Get(ctx, params.SubmissionID)
if err != nil {
return err
}
if submission.Submitter != userId {
return ErrPermissionDeniedNeedRoleSubmissionReview
}
}
data := []byte{}
_, err = req.Read(data)
if err != nil {

BIN
web/public/errors/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
web/public/errors/500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
web/src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,35 @@
import path from 'path';
import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
export async function errorImageResponse(
statusCode: number = 500,
options?: { message?: string }
): Promise<NextResponse> {
const file = `${statusCode}.png`;
const filePath = path.join(process.cwd(), 'public/errors', file);
const headers: Record<string, string> = {
'Content-Type': 'image/png',
};
if (options?.message) {
headers['X-Error-Message'] = encodeURIComponent(options.message);
}
try {
const buffer = await fs.readFile(filePath);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: statusCode,
headers,
});
} catch {
const fallback = path.join(process.cwd(), 'public/errors', '500.png');
const buffer = await fs.readFile(fallback);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: 500,
headers,
});
}
}

View File

@@ -11,10 +11,31 @@ import "./(styles)/page.scss";
import { ListSortConstants } from "../ts/Sort";
export default function MapfixInfoPage() {
const [mapfixes, setMapfixes] = useState<MapfixList>({Total:-1,Mapfixes:[]})
const [mapfixes, setMapfixes] = useState<MapfixList|null>(null)
const [currentPage, setCurrentPage] = useState(1);
const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
useEffect(() => {
async function fetchMapfixes() {
const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
if (res.ok) {
setMapfixes(await res.json())
}
}
setTimeout(() => {
fetchMapfixes()
}, 50);
}, [currentPage])
if (!mapfixes) {
return <Webpage>
<main>
Loading...
</main>
</Webpage>
}
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
const currentCards = mapfixes.Mapfixes.slice(
@@ -34,27 +55,6 @@ export default function MapfixInfoPage() {
}
};
useEffect(() => {
async function fetchMapfixes() {
const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
if (res.ok) {
setMapfixes(await res.json())
}
}
setTimeout(() => {
fetchMapfixes()
}, 50);
}, [currentPage])
if (mapfixes.Total < 0) {
return <Webpage>
<main>
Loading...
</main>
</Webpage>
}
if (mapfixes.Total == 0) {
return <Webpage>
<main>

View File

@@ -9,10 +9,31 @@ import "./(styles)/page.scss";
import { ListSortConstants } from "../ts/Sort";
export default function SubmissionInfoPage() {
const [submissions, setSubmissions] = useState<SubmissionList>({Total:-1,Submissions:[]})
const [submissions, setSubmissions] = useState<SubmissionList|null>(null)
const [currentPage, setCurrentPage] = useState(1);
const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
useEffect(() => {
async function fetchSubmissions() {
const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
if (res.ok) {
setSubmissions(await res.json())
}
}
setTimeout(() => {
fetchSubmissions()
}, 50);
}, [currentPage])
if (!submissions) {
return <Webpage>
<main>
Loading...
</main>
</Webpage>
}
const totalPages = Math.ceil(submissions.Total / cardsPerPage);
const currentCards = submissions.Submissions.slice(
@@ -32,27 +53,6 @@ export default function SubmissionInfoPage() {
}
};
useEffect(() => {
async function fetchSubmissions() {
const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
if (res.ok) {
setSubmissions(await res.json())
}
}
setTimeout(() => {
fetchSubmissions()
}, 50);
}, [currentPage])
if (submissions.Total < 0) {
return <Webpage>
<main>
Loading...
</main>
</Webpage>
}
if (submissions.Total == 0) {
return <Webpage>
<main>

View File

@@ -1,8 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
import { errorImageResponse } from '@/app/lib/errorImageResponse';
const cache = new Map<number, { buffer: Buffer; expires: number }>();
const CACHE_TTL = 15 * 60 * 1000;
setInterval(() => {
const now = Date.now();
for (const [key, value] of cache.entries()) {
if (value.expires <= now) {
cache.delete(key);
}
}
}, 60 * 5 * 1000);
export async function GET(
request: NextRequest,
context: { params: Promise<{ assetId: number }> }
@@ -10,10 +20,9 @@ export async function GET(
const { assetId } = await context.params;
if (!assetId) {
return NextResponse.json(
{ error: 'Missing asset ID' },
{ status: 400 }
);
return errorImageResponse(400, {
message: "Missing asset ID",
})
}
let finalAssetId = assetId;
@@ -31,17 +40,17 @@ export async function GET(
} catch { }
const now = Date.now();
const cached = cache.get(finalAssetId);
const cached = cache.get(finalAssetId);
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
try {
const response = await fetch(
@@ -49,22 +58,21 @@ export async function GET(
);
if (!response.ok) {
throw new Error('Failed to fetch thumbnail JSON');
throw new Error(`Failed to fetch thumbnail JSON [${response.status}]`)
}
const data = await response.json();
const imageUrl = data.data[0]?.imageUrl;
if (!imageUrl) {
return NextResponse.json(
{ error: 'No image URL found in the response' },
{ status: 404 }
);
return errorImageResponse(404, {
message: "No image URL found in the response",
})
}
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error('Failed to fetch the image');
throw new Error(`Failed to fetch the image [${imageResponse.status}]`)
}
const arrayBuffer = await imageResponse.arrayBuffer();
@@ -79,10 +87,9 @@ export async function GET(
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
} catch {
return NextResponse.json(
{ error: 'Failed to fetch or process thumbnail' },
{ status: 500 }
);
} catch (err) {
return errorImageResponse(500, {
message: `Failed to fetch or process thumbnail: ${err}`,
})
}
}

View File

@@ -1,16 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server"
export async function GET(
request: NextRequest,
context: { params: Promise<{ mapId: string }> }
): Promise<NextResponse> {
// TODO: implement this, we need a cdn for in-game map thumbnails...
if (!process.env.API_HOST) {
throw new Error('env variable "API_HOST" is not set')
}
const { mapId } = await context.params;
const { mapId } = await context.params
const protocol = request.headers.get("x-forwarded-proto") || "https";
const host = request.headers.get("host");
const origin = `${protocol}://${host}`;
const apiHost = process.env.API_HOST.replace(/\/api\/?$/, "")
const redirectPath = `/thumbnails/asset/${mapId}`
const redirectUrl = `${apiHost}${redirectPath}`
return NextResponse.redirect(`${origin}/thumbnails/asset/${mapId}`);
return NextResponse.redirect(redirectUrl)
}

View File

@@ -8,25 +8,26 @@ export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl
if (pathname.startsWith("/api")) {
if (!process.env.BASE_URL) {
throw new Error('env variable "BASE_URL" is not set')
if (!process.env.API_HOST) {
throw new Error('env variable "API_HOST" is not set')
}
const baseUrl = process.env.BASE_URL.replace(/\/$/, "");
const apiUrl = new URL(baseUrl + pathname + search);
const baseUrl = process.env.API_HOST.replace(/\/$/, "");
const path = pathname.replace(/^\/api/, "");
const apiUrl = new URL(baseUrl + path + search);
return NextResponse.rewrite(apiUrl, { request });
} else if (pathname.startsWith("/auth")) {
if (!process.env.AUTH_HOST) {
throw new Error('env variable "AUTH_HOST" is not set')
}
const authHost = process.env.AUTH_HOST.replace(/\/$/, "");
const path = pathname.replace(/^\/auth/, "");
const redirectUrl = new URL(authHost + path + search);
return NextResponse.redirect(redirectUrl, 302);
}
}
return NextResponse.next()
}
}