serve frontend

This commit is contained in:
2025-05-15 12:47:15 +08:00
parent d6fea884cf
commit 578aab36c3
24 changed files with 400 additions and 155 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
*.iml *.iml
test.db test.db
.idea/ .idea/
build/

15
build.py Normal file
View File

@@ -0,0 +1,15 @@
import subprocess
import os
import shutil
if os.path.exists("build"):
shutil.rmtree("build")
os.mkdir("build")
subprocess.run(["go", "build", "-o", "build/", "main.go"])
os.chdir("./frontend")
subprocess.run(["npm", "install"], shell=True)
subprocess.run(["npm", "run", "build"], shell=True)
os.chdir("..")
shutil.copytree("./frontend/dist", "./build/static")

View File

@@ -1,11 +1,41 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="{{Description}}">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<!-- SEO meta -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{Title}}">
<meta name="twitter:description" content="{{Description}}">
<meta name="twitter:image" content="{{Preview}}">
<meta property="og:title" content="{{Title}}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{Url}}">
<meta property="og:image" content="{{Preview}}">
<meta property="og:description" content="{{Description}}">
<meta property="og:site_name" content={{SiteName}}>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Nysoure">
<link rel="apple-touch-icon" href="/icon-192.png">
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<title>{{Title}}</title>
</head> </head>
<body> <body>
<script>
window.serverName = "{{SiteName}}";
window.cloudflareTurnstileSiteKey = "{{CFTurnstileSiteKey}}";
</script>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,12 +1,19 @@
import {User} from "./network/models.ts"; import {User} from "./network/models.ts";
interface MyWindow extends Window {
serverName?: string;
cloudflareTurnstileSiteKey?: string;
}
class App { class App {
appName = "资源库" appName = "Nysoure"
user: User | null = null; user: User | null = null;
token: string | null = null; token: string | null = null;
cloudflareTurnstileSiteKey: string | null = null;
constructor() { constructor() {
this.init(); this.init();
} }
@@ -20,6 +27,8 @@ class App {
if (tokenJson) { if (tokenJson) {
this.token = JSON.parse(tokenJson); this.token = JSON.parse(tokenJson);
} }
this.appName = (window as MyWindow).serverName || this.appName;
this.cloudflareTurnstileSiteKey = (window as MyWindow).cloudflareTurnstileSiteKey || null;
} }
saveData() { saveData() {

View File

@@ -10,7 +10,7 @@ export function ErrorAlert({ message, className }: { message: string, className?
export function InfoAlert({ message, className }: { message: string, className?: string }) { export function InfoAlert({ message, className }: { message: string, className?: string }) {
return <div role="alert" className={`alert alert-info ${className}`}> return <div role="alert" className={`alert alert-info ${className}`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
<span>{message}</span> <span>{message}</span>

View File

@@ -32,7 +32,7 @@ export default function Navigator() {
<SearchBar /> <SearchBar />
<UploadingSideBar /> <UploadingSideBar />
{ {
app.isAdmin() && <button className={"btn btn-circle btn-ghost"} onClick={() => { app.isLoggedIn() && <button className={"btn btn-circle btn-ghost"} onClick={() => {
navigate("/manage"); navigate("/manage");
}}> }}>
<MdSettings size={24} /> <MdSettings size={24} />

View File

@@ -116,4 +116,6 @@ export interface ServerConfig {
allow_register: boolean; allow_register: boolean;
cloudflare_turnstile_site_key: string; cloudflare_turnstile_site_key: string;
cloudflare_turnstile_secret_key: string; cloudflare_turnstile_secret_key: string;
server_name: string;
server_description: string;
} }

View File

@@ -1,6 +1,12 @@
import { useEffect } from "react";
import ResourcesView from "../components/resources_view.tsx"; import ResourcesView from "../components/resources_view.tsx";
import {network} from "../network/network.ts"; import {network} from "../network/network.ts";
import { app } from "../app.ts";
export default function HomePage() { export default function HomePage() {
useEffect(() => {
document.title = app.appName;
}, [])
return <ResourcesView loader={(page) => network.getResources(page)}></ResourcesView> return <ResourcesView loader={(page) => network.getResources(page)}></ResourcesView>
} }

View File

@@ -1,4 +1,4 @@
import {FormEvent, useState} from "react"; import {FormEvent, useEffect, useState} from "react";
import {network} from "../network/network.ts"; import {network} from "../network/network.ts";
import {app} from "../app.ts"; import {app} from "../app.ts";
import {useNavigate} from "react-router"; import {useNavigate} from "react-router";
@@ -32,6 +32,10 @@ export default function LoginPage() {
} }
}; };
useEffect(() => {
document.title = t("Login");
}, [])
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"login-page"}> return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"login-page"}>
<div className={"w-96 card card-border bg-base-100 border-base-300"}> <div className={"w-96 card card-border bg-base-100 border-base-300"}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>

View File

@@ -24,6 +24,10 @@ export default function ManagePage() {
}; };
}, []); }, []);
useEffect(() => {
document.title = t("Manage");
}, [])
const buildItem = (title: string, icon: ReactNode, p: number) => { const buildItem = (title: string, icon: ReactNode, p: number) => {
return <li key={title} onClick={() => { return <li key={title} onClick={() => {
setPage(p); setPage(p);

View File

@@ -62,7 +62,7 @@ export default function ManageServerConfigPage() {
setIsLoading(false); setIsLoading(false);
}; };
return <form className="px-4" onSubmit={handleSubmit}> return <form className="px-4 pb-4" onSubmit={handleSubmit}>
<Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => { <Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => {
setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) }) setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) })
}}></Input> }}></Input>
@@ -78,6 +78,12 @@ export default function ManageServerConfigPage() {
setConfig({ ...config, allow_register: e.target.checked }) setConfig({ ...config, allow_register: e.target.checked })
}} /> }} />
</fieldset> </fieldset>
<Input type="text" value={config.server_name} label="Server name" onChange={(e) => {
setConfig({...config, server_name: e.target.value })
}}></Input>
<Input type="text" value={config.server_description} label="Server description" onChange={(e) => {
setConfig({...config, server_description: e.target.value })
}}></Input>
<Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => { <Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => {
setConfig({...config, cloudflare_turnstile_site_key: e.target.value }) setConfig({...config, cloudflare_turnstile_site_key: e.target.value })
}}></Input> }}></Input>

View File

@@ -1,4 +1,4 @@
import {useRef, useState} from "react"; import { useEffect, useRef, useState } from "react";
import { MdAdd, MdDelete, MdOutlineInfo } from "react-icons/md"; import { MdAdd, MdDelete, MdOutlineInfo } from "react-icons/md";
import { Tag } from "../network/models.ts"; import { Tag } from "../network/models.ts";
import { network } from "../network/network.ts"; import { network } from "../network/network.ts";
@@ -22,6 +22,10 @@ export default function PublishPage() {
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
document.title = t("Publish Resource");
}, [])
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting) { if (isSubmitting) {
return return

View File

@@ -1,4 +1,4 @@
import {FormEvent, useState} from "react"; import {FormEvent, useEffect, useState} from "react";
import {network} from "../network/network.ts"; import {network} from "../network/network.ts";
import {app} from "../app.ts"; import {app} from "../app.ts";
import {useNavigate} from "react-router"; import {useNavigate} from "react-router";
@@ -37,6 +37,10 @@ export default function RegisterPage() {
} }
}; };
useEffect(() => {
document.title = t("Register");
}, [])
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"register-page"}> return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"register-page"}>
<div className={"w-96 card card-border bg-base-100 border-base-300"}> <div className={"w-96 card card-border bg-base-100 border-base-300"}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>

View File

@@ -37,11 +37,16 @@ export default function ResourcePage() {
} }
}, [id]) }, [id])
useEffect(() => {
document.title = t("Resource Details");
}, [])
useEffect(() => { useEffect(() => {
if (!isNaN(id)) { if (!isNaN(id)) {
network.getResourceDetails(id).then((res) => { network.getResourceDetails(id).then((res) => {
if (res.success) { if (res.success) {
setResource(res.data!) setResource(res.data!)
document.title = res.data!.title
} else { } else {
showToast({ message: res.message, type: "error" }) showToast({ message: res.message, type: "error" })
} }

View File

@@ -10,7 +10,9 @@ export default function SearchPage() {
const keyword = params.get("keyword") const keyword = params.get("keyword")
useEffect(() => {}, []) useEffect(() => {
document.title = t("Search") + ": " + (keyword || "");
}, [])
if (keyword === null || keyword === "") { if (keyword === null || keyword === "") {
return <div role="alert" className="alert alert-info alert-dash"> return <div role="alert" className="alert alert-info alert-dash">

View File

@@ -2,16 +2,24 @@ import {useParams} from "react-router";
import { ErrorAlert } from "../components/alert.tsx"; import { ErrorAlert } from "../components/alert.tsx";
import ResourcesView from "../components/resources_view.tsx"; import ResourcesView from "../components/resources_view.tsx";
import { network } from "../network/network.ts"; import { network } from "../network/network.ts";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
export default function TaggedResourcesPage() { export default function TaggedResourcesPage() {
const { tag } = useParams() const { tag } = useParams()
const { t } = useTranslation();
if (!tag) { if (!tag) {
return <div> return <div>
<ErrorAlert message={"Tag not found"} /> <ErrorAlert message={"Tag not found"} />
</div> </div>
} }
useEffect(() => {
document.title = t("Tag: " + tag);
}, [tag])
return <div> return <div>
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold"}> <h1 className={"text-2xl pt-6 pb-2 px-4 font-bold"}>
Tag: {tag} Tag: {tag}

View File

@@ -28,6 +28,10 @@ export default function UserPage() {
}); });
}, [username]); }, [username]);
useEffect(() => {
document.title = username || "User";
}, [username]);
if (!user) { if (!user) {
return <div className="w-full"> return <div className="w-full">
<Loading /> <Loading />

2
go.mod
View File

@@ -28,9 +28,11 @@ require (
github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gofiber/schema v1.3.0 // indirect github.com/gofiber/schema v1.3.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.8 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.8 // indirect
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/k3a/html2text v1.2.1
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

13
go.sum
View File

@@ -20,12 +20,18 @@ github.com/gofiber/utils/v2 v2.0.0-beta.8 h1:ZifwbHZqZO3YJsx1ZhDsWnPjaQ7C0YD20LH
github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c= github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -49,6 +55,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
@@ -61,17 +69,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=

View File

@@ -28,6 +28,8 @@ func main() {
app.Use(middleware.JwtMiddleware) app.Use(middleware.JwtMiddleware)
app.Use(middleware.FrontendMiddleware)
if debugMode { if debugMode {
app.Use(cors.New(cors.ConfigDefault)) app.Use(cors.New(cors.ConfigDefault))
} }

View File

@@ -22,6 +22,10 @@ type ServerConfig struct {
CloudflareTurnstileSiteKey string `json:"cloudflare_turnstile_site_key"` CloudflareTurnstileSiteKey string `json:"cloudflare_turnstile_site_key"`
// CloudflareTurnstileSecretKey is the secret key for Cloudflare Turnstile. // CloudflareTurnstileSecretKey is the secret key for Cloudflare Turnstile.
CloudflareTurnstileSecretKey string `json:"cloudflare_turnstile_secret_key"` CloudflareTurnstileSecretKey string `json:"cloudflare_turnstile_secret_key"`
ServerName string `json:"server_name"`
ServerDescription string `json:"server_description"`
} }
func init() { func init() {
@@ -34,6 +38,8 @@ func init() {
AllowRegister: true, AllowRegister: true,
CloudflareTurnstileSiteKey: "", CloudflareTurnstileSiteKey: "",
CloudflareTurnstileSecretKey: "", CloudflareTurnstileSecretKey: "",
ServerName: "Nysoure",
ServerDescription: "Nysoure is a file sharing service.",
} }
} else { } else {
data, err := os.ReadFile(filepath) data, err := os.ReadFile(filepath)
@@ -78,3 +84,15 @@ func AllowRegister() bool {
func MaxDownloadsPerDayForSingleIP() int { func MaxDownloadsPerDayForSingleIP() int {
return config.MaxDownloadsPerDayForSingleIP return config.MaxDownloadsPerDayForSingleIP
} }
func CloudflareTurnstileSiteKey() string {
return config.CloudflareTurnstileSiteKey
}
func ServerName() string {
return config.ServerName
}
func ServerDescription() string {
return config.ServerDescription
}

View File

@@ -0,0 +1,106 @@
package middleware
import (
"fmt"
"nysoure/server/config"
"nysoure/server/service"
"os"
"strconv"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/k3a/html2text"
)
func FrontendMiddleware(c fiber.Ctx) error {
if strings.HasPrefix(c.Path(), "/api") {
return c.Next()
}
path := c.Path()
file := "static" + path
if _, err := os.Stat(file); path == "/" || os.IsNotExist(err) {
return serveIndexHtml(c)
} else {
return c.SendFile(file)
}
}
func serveIndexHtml(c fiber.Ctx) error {
data, err := os.ReadFile("static/index.html")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
content := string(data)
siteName := config.ServerName()
description := config.ServerDescription()
preview := "/icon-192.png"
title := siteName
url := c.OriginalURL()
cfTurnstileSiteKey := config.CloudflareTurnstileSiteKey()
if strings.HasPrefix(url, "/resources/") {
idStr := strings.TrimPrefix(url, "/resources/")
id, err := strconv.Atoi(idStr)
if err == nil {
r, err := service.GetResource(uint(id))
if err == nil {
if len(r.Images) > 0 {
preview = fmt.Sprintf("/images/%d", r.Images[0].ID)
}
title = r.Title
description = getResourceDescription(r.Article)
}
}
} else if strings.HasPrefix(url, "/user/") {
username := strings.TrimPrefix(url, "/user/")
u, err := service.GetUserByUsername(username)
if err == nil {
preview = fmt.Sprintf("/avatar/%d", u.ID)
title = u.Username
description = "User " + u.Username + "'s profile"
}
}
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
content = strings.ReplaceAll(content, "{{Description}}", description)
content = strings.ReplaceAll(content, "{{Preview}}", preview)
content = strings.ReplaceAll(content, "{{Title}}", title)
content = strings.ReplaceAll(content, "{{Url}}", url)
content = strings.ReplaceAll(content, "{{CFTurnstileSiteKey}}", cfTurnstileSiteKey)
c.Set("Content-Type", "text/html; charset=utf-8")
return c.SendString(content)
}
func getResourceDescription(article string) string {
htmlContent := mdToHTML([]byte(article))
plain := html2text.HTML2Text(string(htmlContent))
if len([]rune(plain)) > 100 {
plain = string([]rune(plain)[:100])
}
plain = strings.ReplaceAll(plain, "\n", " ")
plain = strings.ReplaceAll(plain, "\r", "")
plain = strings.ReplaceAll(plain, "\t", "")
plain = strings.TrimSpace(plain)
return plain
}
func mdToHTML(md []byte) []byte {
// create Markdown parser with extensions
extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock | parser.MathJax
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)
// create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return markdown.Render(doc, renderer)
}