Compare commits

...
7 Commits
Author SHA1 Message Date
fristy ff18fc353d Domain degisikligi: fristy.net -> fristy.dev
CI / Code Health (pull_request) Successful in 22s
CI / Lint, Test & Build (pull_request) Successful in 46s
CI / Code Health (push) Successful in 25s
CI / Lint, Test & Build (push) Successful in 54s
Deploy / Build & Push Image (push) Successful in 56s
Deploy / Trigger Deploy (push) Successful in 1s
2026-10-15 21:48:52 +03:00
OpenCode 0c30c18e24 Merge pull request 'desloppify: code health overhaul plus abuse hardening' (#3) from desloppify/code-health into main
CI / Lint, Test & Build (push) Successful in 1m25s
CI / Code Health (push) Successful in 16s
Deploy / Build & Push Image (push) Successful in 48s
Deploy / Trigger Deploy (push) Successful in 1s
Merge PR #3: code health overhaul plus abuse hardening, 140 vitest green, CI split preserved
2026-09-12 00:58:23 +03:00
fristy ed149ded52 fix: prettier formatting and downgrade upload-artifact to v3 for Gitea
CI / Code Health (pull_request) Successful in 24s
CI / Lint, Test & Build (pull_request) Successful in 59s
2026-09-12 00:47:38 +03:00
fristy 7fa3a94278 Merge remote-tracking branch 'origin/main' into desloppify/code-health
CI / Lint, Test & Build (pull_request) Failing after 39s
CI / Code Health (pull_request) Failing after 15s
2026-09-12 00:42:44 +03:00
OpenCode 5837e77be8 Merge pull request 'ci: split pipeline into ci plus deploy' (#2) from ci/split-pipeline into main
CI / Lint, Test & Build (push) Failing after 43s
CI / Code Health (push) Successful in 15s
Deploy / Build & Push Image (push) Successful in 49s
Deploy / Trigger Deploy (push) Successful in 1s
2026-09-12 00:40:32 +03:00
fristy 0504baf77b ci: fix desloppify scan path for CI profile
CI / Lint, Test & Build (pull_request) Failing after 43s
CI / Code Health (pull_request) Successful in 14s
2026-09-12 00:36:46 +03:00
fristy 62b1aa9e33 ci: split pipeline into ci plus deploy, add test and health jobs
CI / Code Health (pull_request) Successful in 58s
CI / Lint, Test & Build (pull_request) Failing after 44s
2026-09-12 00:24:23 +03:00
26 changed files with 138 additions and 95 deletions

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
name: CI
on:
pull_request:
push:
branches: ["main"]
jobs:
check:
name: Lint, Test & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
- name: Build
run: pnpm build
health:
name: Code Health
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Scan with desloppify (CI profile, objective only)
continue-on-error: true
run: |
pipx run --spec desloppify desloppify scan --profile ci --path . --no-badge
- name: Upload scorecard
uses: actions/upload-artifact@v3
with:
name: desloppify-scorecard
path: scorecard.png
if-no-files-found: ignore
@@ -1,51 +1,21 @@
name: Build & Push name: Deploy
on: on:
push: push:
branches: ["main"] branches: ["main"]
tags: ["v*"] tags: ["v*"]
pull_request:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
env: env:
REGISTRY: git.fristy.net REGISTRY: git.fristy.net
IMAGE_NAME: fristy/fristynet IMAGE_NAME: fristy/fristydev
jobs: jobs:
check:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
image: image:
name: Build & Push Image name: Build & Push Image
needs: check
if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -79,7 +49,7 @@ jobs:
deploy: deploy:
name: Trigger Deploy name: Trigger Deploy
needs: image needs: image
if: github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Trigger Deploy - name: Trigger Deploy
+5 -5
View File
@@ -1,10 +1,10 @@
# fristy.net # fristy.dev
![Code Health](scorecard.png) ![Code Health](scorecard.png)
My personal corner of the internet — projects, live Discord status, and ways to reach me. My personal corner of the internet — projects, live Discord status, and ways to reach me.
**Live:** https://fristy.net **Live:** https://fristy.dev
## What's inside ## What's inside
@@ -48,15 +48,15 @@ docker run --rm -p 3000:3000 fristynet
No need to build it yourself though — CI publishes every `main` push: No need to build it yourself though — CI publishes every `main` push:
```bash ```bash
docker pull git.fristy.net/fristy/fristynet:latest docker pull git.fristy.net/fristy/fristydev:latest
``` ```
## CI/CD ## CI/CD
Pushes to `main` (and `<sha>` tags) run format check, lint and build first. If those pass, the Docker image is built and pushed to the Gitea registry: Pushes to `main` (and `<sha>` tags) run format check, lint and build first. If those pass, the Docker image is built and pushed to the Gitea registry:
- `git.fristy.net/fristy/fristynet:latest` on every `main` push - `git.fristy.net/fristy/fristydev:latest` on every `main` push
- `git.fristy.net/fristy/fristynet:<sha>` for every pushed commit - `git.fristy.net/fristy/fristydev:<sha>` for every pushed commit
Pull requests only run the checks — nothing is pushed. Pull requests only run the checks — nothing is pushed.
+3 -3
View File
@@ -13,7 +13,7 @@ function contextWith(id: string) {
describe("GET /api/discord-app/[id]", () => { describe("GET /api/discord-app/[id]", () => {
it("returns 400 for non-numeric ids", async () => { it("returns 400 for non-numeric ids", async () => {
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/abc"), new Request("https://fristy.dev/api/discord-app/abc"),
contextWith("abc"), contextWith("abc"),
); );
expect(response.status).toBe(400); expect(response.status).toBe(400);
@@ -25,7 +25,7 @@ describe("GET /api/discord-app/[id]", () => {
vi.fn(async () => ({ ok: false, status: 404 })), vi.fn(async () => ({ ok: false, status: 404 })),
); );
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/123"), new Request("https://fristy.dev/api/discord-app/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(404); expect(response.status).toBe(404);
@@ -39,7 +39,7 @@ describe("GET /api/discord-app/[id]", () => {
}), }),
); );
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/123"), new Request("https://fristy.dev/api/discord-app/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(502); expect(response.status).toBe(502);
+3 -1
View File
@@ -22,7 +22,9 @@ export async function GET(
try { try {
info = await getDiscordAppInfo(id); info = await getDiscordAppInfo(id);
} catch (error) { } catch (error) {
warnDev(`discord-app upstream failed: ${id} -> ${error instanceof Error ? error.message : error}`); warnDev(
`discord-app upstream failed: ${id} -> ${error instanceof Error ? error.message : error}`,
);
return jsonErrorWithCors({ error: "Upstream unavailable" }, 502, 60, origin); return jsonErrorWithCors({ error: "Upstream unavailable" }, 502, 60, origin);
} }
if (!info) { if (!info) {
+5 -5
View File
@@ -17,11 +17,11 @@ function requestWith(url: string) {
describe("GET /api/game/[id]", () => { describe("GET /api/game/[id]", () => {
it("returns 400 for non-numeric ids", async () => { it("returns 400 for non-numeric ids", async () => {
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/abc"), requestWith("https://fristy.dev/api/game/abc"),
contextWith("abc"), contextWith("abc"),
); );
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
it("returns 404 when no profile resolves", async () => { it("returns 404 when no profile resolves", async () => {
@@ -30,7 +30,7 @@ describe("GET /api/game/[id]", () => {
vi.fn(async () => ({ ok: false, status: 404 })), vi.fn(async () => ({ ok: false, status: 404 })),
); );
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/123"), requestWith("https://fristy.dev/api/game/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(404); expect(response.status).toBe(404);
@@ -44,10 +44,10 @@ describe("GET /api/game/[id]", () => {
}), }),
); );
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/123"), requestWith("https://fristy.dev/api/game/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(502); expect(response.status).toBe(502);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
}); });
+1 -1
View File
@@ -17,7 +17,7 @@ export async function generateMetadata(): Promise<Metadata> {
metadataBase: new URL(SITE_URL), metadataBase: new URL(SITE_URL),
title: { title: {
default: dict.meta.title, default: dict.meta.title,
template: "%s · fristy.net", template: "%s · fristy.dev",
}, },
description: dict.meta.description, description: dict.meta.description,
openGraph: { openGraph: {
+3 -1
View File
@@ -285,7 +285,9 @@ export function GameProfileModal({
</dl> </dl>
) : null} ) : null}
{profile ? <ModalReviews profile={profile} dict={dict} localeTag={localeTag} /> : null} {profile ? (
<ModalReviews profile={profile} dict={dict} localeTag={localeTag} />
) : null}
{profile?.itad && (bestPrice || historyLow) ? ( {profile?.itad && (bestPrice || historyLow) ? (
<ModalPrices <ModalPrices
+5 -1
View File
@@ -9,7 +9,11 @@ describe("ModalReviews", () => {
it("renders nothing without review sources", () => { it("renders nothing without review sources", () => {
const dict = getDictionary("en"); const dict = getDictionary("en");
const { container } = render( const { container } = render(
<ModalReviews profile={profileFixture()} dict={dict.discord.gameModal} localeTag="en-US" />, <ModalReviews
profile={profileFixture()}
dict={dict.discord.gameModal}
localeTag="en-US"
/>,
); );
expect(container.textContent).toBe(""); expect(container.textContent).toBe("");
}); });
+1 -1
View File
@@ -7,7 +7,7 @@ describe("SiteFooter", () => {
it("renders brand, rights, and source links", () => { it("renders brand, rights, and source links", () => {
const dict = getDictionary("en"); const dict = getDictionary("en");
const { container } = render(<SiteFooter dict={dict.footer} />); const { container } = render(<SiteFooter dict={dict.footer} />);
expect(container.querySelector('a[href="https://fristy.net"]')).not.toBeNull(); expect(container.querySelector('a[href="https://fristy.dev"]')).not.toBeNull();
expect(container.textContent).toContain(dict.footer.rights); expect(container.textContent).toContain(dict.footer.rights);
const links = screen.getAllByRole("link"); const links = screen.getAllByRole("link");
expect(links.length).toBeGreaterThanOrEqual(3); expect(links.length).toBeGreaterThanOrEqual(3);
+1 -1
View File
@@ -28,7 +28,7 @@ export function SiteFooter({ dict }: { dict: Dictionary["footer"] }) {
<p> <p>
© {year}{" "} © {year}{" "}
<a href={SITE_URL} className="transition-colors hover:text-white"> <a href={SITE_URL} className="transition-colors hover:text-white">
fristy.net fristy.dev
</a>{" "} </a>{" "}
· {dict.rights} · {dict.rights}
</p> </p>
+1 -1
View File
@@ -7,7 +7,7 @@ export function SiteHeader({ dict, locale }: { dict: Dictionary["nav"]; locale:
<header className="sticky top-0 z-50 border-b border-white/5 bg-void/80 backdrop-blur"> <header className="sticky top-0 z-50 border-b border-white/5 bg-void/80 backdrop-blur">
<div className="mx-auto flex h-16 w-full max-w-4xl items-center justify-between px-5"> <div className="mx-auto flex h-16 w-full max-w-4xl items-center justify-between px-5">
<a href="#top" className="font-mono text-lg font-bold tracking-tight"> <a href="#top" className="font-mono text-lg font-bold tracking-tight">
fristy<span className="text-blurple">.net</span> fristy<span className="text-blurple">.dev</span>
</a> </a>
<nav className="hidden items-center gap-6 text-sm text-zinc-400 sm:flex"> <nav className="hidden items-center gap-6 text-sm text-zinc-400 sm:flex">
<a href="#discord" className="transition-colors hover:text-white"> <a href="#discord" className="transition-colors hover:text-white">
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
web: web:
image: git.fristy.net/fristy/fristynet:latest image: git.fristy.net/fristy/fristydev:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID} TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID}
+3 -3
View File
@@ -29,7 +29,7 @@ describe("jsonErrorWithCors", () => {
const response = jsonErrorWithCors({ error: "Nope" }, 404); const response = jsonErrorWithCors({ error: "Nope" }, 404);
expect(response.status).toBe(404); expect(response.status).toBe(404);
expect(response.headers.get("Cache-Control")).toContain("s-maxage=60"); expect(response.headers.get("Cache-Control")).toContain("s-maxage=60");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(response.json()).resolves.toEqual({ error: "Nope" }); await expect(response.json()).resolves.toEqual({ error: "Nope" });
}); });
}); });
@@ -37,7 +37,7 @@ describe("jsonWithCors", () => {
it("sets cache and CORS headers", async () => { it("sets cache and CORS headers", async () => {
const response = jsonWithCors({ ok: true }, 3600); const response = jsonWithCors({ ok: true }, 3600);
expect(response.headers.get("Cache-Control")).toContain("s-maxage=3600"); expect(response.headers.get("Cache-Control")).toContain("s-maxage=3600");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(response.json()).resolves.toEqual({ ok: true }); await expect(response.json()).resolves.toEqual({ ok: true });
}); });
@@ -48,6 +48,6 @@ describe("jsonWithCors", () => {
it("falls back to production for untrusted origins", async () => { it("falls back to production for untrusted origins", async () => {
const response = jsonWithCors({ ok: true }, 3600, "https://evil.example"); const response = jsonWithCors({ ok: true }, 3600, "https://evil.example");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
}); });
+2 -9
View File
@@ -22,10 +22,7 @@ export function isAllowedOrigin(request: Request): boolean {
} }
function resolveAllowedOrigin(origin: string | null): string { function resolveAllowedOrigin(origin: string | null): string {
if ( if (origin !== null && (origin === ALLOWED_ORIGIN || (isDev() && DEV_ORIGINS.has(origin)))) {
origin !== null &&
(origin === ALLOWED_ORIGIN || (isDev() && DEV_ORIGINS.has(origin)))
) {
return origin; return origin;
} }
return ALLOWED_ORIGIN; return ALLOWED_ORIGIN;
@@ -79,11 +76,7 @@ export function jsonErrorWithCors(
}, },
}); });
} }
export function jsonWithCors( export function jsonWithCors(data: unknown, sMaxage = 3600, origin?: string | null): NextResponse {
data: unknown,
sMaxage = 3600,
origin?: string | null,
): NextResponse {
return NextResponse.json(data, { return NextResponse.json(data, {
headers: { headers: {
"Cache-Control": `public, s-maxage=${sMaxage}, stale-while-revalidate=86400`, "Cache-Control": `public, s-maxage=${sMaxage}, stale-while-revalidate=86400`,
+1 -1
View File
@@ -57,7 +57,7 @@ export async function getDiscordAppInfo(applicationId: string): Promise<DiscordA
let response: Response; let response: Response;
try { try {
response = await fetch(`https://discord.com/api/v10/applications/${applicationId}/rpc`, { response = await fetch(`https://discord.com/api/v10/applications/${applicationId}/rpc`, {
headers: { "User-Agent": "fristy.net (personal site)" }, headers: { "User-Agent": "fristy.dev (personal site)" },
next: { revalidate: 86_400 }, next: { revalidate: 86_400 },
}); });
} catch (error) { } catch (error) {
+5 -1
View File
@@ -69,7 +69,11 @@ export function formatReleaseDate(value: string, localeTag: string): string {
return value; return value;
} }
export function formatCount(value: number | null | undefined, localeTag: string, unit: string): string { export function formatCount(
value: number | null | undefined,
localeTag: string,
unit: string,
): string {
if (value == null) { if (value == null) {
return "—"; return "—";
} }
+6 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildLinks, mergePeople, mergePlatforms, selectScreenshots } from "@/lib/game/game-profile"; import {
buildLinks,
mergePeople,
mergePlatforms,
selectScreenshots,
} from "@/lib/game/game-profile";
import type { IgdbGame } from "@/lib/game/game-profile-types"; import type { IgdbGame } from "@/lib/game/game-profile-types";
describe("mergePeople", () => { describe("mergePeople", () => {
+4 -5
View File
@@ -186,7 +186,9 @@ export function toPricePoint(p: ItadOverviewPrice["current"]): GamePricePoint |
export function mapItadBlock( export function mapItadBlock(
itadInfo: ItadGameInfo | null, itadInfo: ItadGameInfo | null,
priceEntry: { current?: ItadOverviewPrice["current"]; lowest?: ItadOverviewPrice["current"] } | undefined, priceEntry:
| { current?: ItadOverviewPrice["current"]; lowest?: ItadOverviewPrice["current"] }
| undefined,
): NonNullable<GameProfile["itad"]> | null { ): NonNullable<GameProfile["itad"]> | null {
if (!process.env.ITAD_API_KEY || (!itadInfo && !priceEntry)) { if (!process.env.ITAD_API_KEY || (!itadInfo && !priceEntry)) {
return null; return null;
@@ -197,10 +199,7 @@ export function mapItadBlock(
pageUrl: itadInfo?.urls?.game ?? null, pageUrl: itadInfo?.urls?.game ?? null,
playerCount: itadInfo?.players?.recent ?? null, playerCount: itadInfo?.players?.recent ?? null,
reviews: (itadInfo?.reviews ?? []) reviews: (itadInfo?.reviews ?? [])
.filter( .filter((r): r is ItadReview & { source: string; url: string } => !!r.source && !!r.url)
(r): r is ItadReview & { source: string; url: string } =>
!!r.source && !!r.url,
)
.map((r) => ({ .map((r) => ({
score: r.score ?? null, score: r.score ?? null,
source: r.source, source: r.source,
+3 -3
View File
@@ -25,9 +25,9 @@ describe("activityImageUrl", () => {
}); });
it("resolves Spotify keys", () => { it("resolves Spotify keys", () => {
expect(activityImageUrl(activity({ assets: { large_image: "spotify:img" } }), "large")).toBe( expect(
"https://i.scdn.co/image/img", activityImageUrl(activity({ assets: { large_image: "spotify:img" } }), "large"),
); ).toBe("https://i.scdn.co/image/img");
}); });
it("resolves app assets with application id", () => { it("resolves app assets with application id", () => {
+4 -1
View File
@@ -5,6 +5,8 @@ const GITEA_USER = "fristy";
const EXCLUDED_REPOS = new Set([".profile"]); const EXCLUDED_REPOS = new Set([".profile"]);
const MAX_REPOS = 8;
export interface GiteaRepo { export interface GiteaRepo {
id: number; id: number;
name: string; name: string;
@@ -56,7 +58,8 @@ export async function getRepos(): Promise<GiteaRepo[]> {
forks: repo.forks_count, forks: repo.forks_count,
updatedAt: repo.updated_at, updatedAt: repo.updated_at,
})) }))
.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)); .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))
.slice(0, MAX_REPOS);
} catch { } catch {
warnDev("cannot fetch gitea repos"); warnDev("cannot fetch gitea repos");
return []; return [];
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net — fristy love you", "title": "fristy.dev — fristy love you",
"description": "Fristys persönliche Ecke: Projekte, Live-Discord-Status und Kontakt." "description": "Fristys persönliche Ecke: Projekte, Live-Discord-Status und Kontakt."
}, },
"nav": { "nav": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net — fristy love you", "title": "fristy.dev — fristy love you",
"description": "Fristy's personal corner: projects, live Discord status and contact." "description": "Fristy's personal corner: projects, live Discord status and contact."
}, },
"nav": { "nav": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net - fristy love you", "title": "fristy.dev - fristy love you",
"description": "Fristy'nin kişisel köşesi: projeler, canlı Discord durumu ve iletişim." "description": "Fristy'nin kişisel köşesi: projeler, canlı Discord durumu ve iletişim."
}, },
"nav": { "nav": {
@@ -71,7 +71,7 @@
}, },
"projects": { "projects": {
"title": "Projeler", "title": "Projeler",
"subtitle": "Açık kaynak projelerim git.fristy.net üzerinde. Liste otomatik güncelleniyor.", "subtitle": "Açık kaynak projelerim git.fristy.net adresinde barınıyor. Liste her saat otomatik güncelleniyor.",
"updated": "Güncellendi", "updated": "Güncellendi",
"stars": "Yıldız", "stars": "Yıldız",
"forks": "Çatal", "forks": "Çatal",
+3 -3
View File
@@ -1,7 +1,7 @@
export const SITE_URL = "https://fristy.net"; export const SITE_URL = "https://fristy.dev";
export const CONTACT_EMAIL = "iletisim@fristy.net"; export const CONTACT_EMAIL = "iletisim@fristy.dev";
export const GITEA_PROFILE_URL = "https://git.fristy.net/fristy"; export const GITEA_PROFILE_URL = "https://git.fristy.net/fristy";
export const SOURCE_URL = "https://git.fristy.net/fristy/fristynet"; export const SOURCE_URL = "https://git.fristy.net/fristy/fristydev";
export const GITHUB_URL = "https://github.com/fristyxd"; export const GITHUB_URL = "https://github.com/fristyxd";
export const DISCORD_SERVER_URL = "https://discord.gg/fristy"; export const DISCORD_SERVER_URL = "https://discord.gg/fristy";
export const SPOTIFY_URL = "https://open.spotify.com/user/gayiw5j9i0rtvyra6gfzhpfwf"; export const SPOTIFY_URL = "https://open.spotify.com/user/gayiw5j9i0rtvyra6gfzhpfwf";
+6 -6
View File
@@ -9,9 +9,9 @@ afterEach(() => {
function apiRequest(path: string, init?: { method?: string; origin?: string | null }): NextRequest { function apiRequest(path: string, init?: { method?: string; origin?: string | null }): NextRequest {
const headers = new Headers(); const headers = new Headers();
if (init?.origin !== null) { if (init?.origin !== null) {
headers.set("origin", init?.origin ?? "https://fristy.net"); headers.set("origin", init?.origin ?? "https://fristy.dev");
} }
return new NextRequest(`https://fristy.net${path}`, { return new NextRequest(`https://fristy.dev${path}`, {
method: init?.method ?? "GET", method: init?.method ?? "GET",
headers, headers,
}); });
@@ -21,19 +21,19 @@ describe("proxy /api/*", () => {
it("answers OPTIONS preflight with CORS headers", () => { it("answers OPTIONS preflight with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1", { method: "OPTIONS" })); const response = proxy(apiRequest("/api/game/1", { method: "OPTIONS" }));
expect(response.status).toBe(204); expect(response.status).toBe(204);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS"); expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS");
}); });
it("rejects foreign origins", () => { it("rejects foreign origins", () => {
const response = proxy(apiRequest("/api/game/1", { origin: "https://evil.test" })); const response = proxy(apiRequest("/api/game/1", { origin: "https://evil.test" }));
expect(response.status).toBe(403); expect(response.status).toBe(403);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
it("passes same-origin API requests with CORS headers", () => { it("passes same-origin API requests with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1")); const response = proxy(apiRequest("/api/game/1"));
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
}); });
@@ -57,7 +57,7 @@ describe("proxy rate limit", () => {
const limited = proxy(ipRequest("10.0.0.2")); const limited = proxy(ipRequest("10.0.0.2"));
expect(limited.status).toBe(429); expect(limited.status).toBe(429);
expect(limited.headers.get("Retry-After")).toBe("60"); expect(limited.headers.get("Retry-After")).toBe("60");
expect(limited.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(limited.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(limited.json()).resolves.toEqual({ error: "Too many requests" }); await expect(limited.json()).resolves.toEqual({ error: "Too many requests" });
}); });