diff --git a/README.md b/README.md
index 3e2b8b5..b1d42f6 100644
--- a/README.md
+++ b/README.md
@@ -80,8 +80,9 @@ If your configuration is not valid, sqtracker will fail to start.
| SQ_BP_EARNED_PER_GB | envs | 1 | Number of bonus points awarded to a user for each GB they upload |
| SQ_BP_COST_PER_INVITE | envs | 3 | Number of bonus it costs a user to buy 1 invite (set to 0 to disable buying invites) |
| SQ_BP_COST_PER_GB | envs | 3 | Number of bonus it costs a user to buy 1 GB of upload (set to 0 to disable buying upload) |
-| SQ_SITE_WIDE_FREELEECH | envs | `true` | Whether or not to enable freeleech on all torrents |
+| SQ_SITE_WIDE_FREELEECH | envs | `false` | Whether or not to enable freeleech on all torrents |
| SQ_TORRENT_CATEGORIES | envs | `{ "Movies": ["HD", ...], "TV": [...] }` | A dictionary of categories, each with an array of zero or more sources available within that category |
+| SQ_ALLOW_UNREGISTERED_VIEW | envs | `false` | Whether or not torrent pages can be viewed by unregistered users. If true, only logged in users will be able to download/interact, but anyone (search engines included) will be able to view/read torrent info. |
| SQ_BASE_URL | envs | https://demo.sqtracker.dev | The URL of your tracker site |
| SQ_API_URL | envs | https://demo.sqtracker.dev/api | The URL of your API. Under the recommended setup, it should be `${SQ_BASE_URL}/api` |
| SQ_MONGO_URL | envs | mongodb://sq_mongodb/sq | The URL of your MongoDB server. Under the recommended setup, it should be `mongodb://sq_mongodb/sq` |
diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js
index 0461de5..6ba7c9e 100644
--- a/api/src/controllers/torrent.js
+++ b/api/src/controllers/torrent.js
@@ -671,6 +671,15 @@ export const listLatest = (tracker) => async (req, res, next) => {
}
};
+export const listAll = async (req, res, next) => {
+ try {
+ const torrents = await Torrent.find({}, { infoHash: 1 }).lean();
+ res.json(torrents);
+ } catch (e) {
+ next(e);
+ }
+};
+
export const searchTorrents = (tracker) => async (req, res, next) => {
const { query, category, source, tag, page } = req.query;
try {
diff --git a/api/src/index.js b/api/src/index.js
index b7f7aad..e6e3895 100644
--- a/api/src/index.js
+++ b/api/src/index.js
@@ -122,7 +122,7 @@ validateConfig(config).then(() => {
keyGenerator: (req) => {
if (
req.headers["x-forwarded-for"] &&
- req.headers["x-sq-server-secret"] === process.env.SQ_SERVER_SECET
+ req.headers["x-sq-server-secret"] === process.env.SQ_SERVER_SECRET
)
return req.headers["x-forwarded-for"].split(",")[0];
return req.ip;
diff --git a/api/src/middleware/auth.js b/api/src/middleware/auth.js
index 9b2a81a..4da4b86 100644
--- a/api/src/middleware/auth.js
+++ b/api/src/middleware/auth.js
@@ -25,6 +25,11 @@ const auth = async (req, res, next) => {
} catch (err) {
res.status(500).send(err);
}
+ } else if (
+ req.headers["x-sq-public-access"] === "true" &&
+ req.headers["x-sq-server-secret"] === process.env.SQ_SERVER_SECRET
+ ) {
+ next();
} else {
res.sendStatus(401);
}
diff --git a/api/src/routes/torrent.js b/api/src/routes/torrent.js
index 5c30dd3..f4f17ab 100644
--- a/api/src/routes/torrent.js
+++ b/api/src/routes/torrent.js
@@ -5,6 +5,7 @@ import {
deleteTorrent,
fetchTorrent,
listLatest,
+ listAll,
removeVote,
searchTorrents,
toggleFreeleech,
@@ -28,6 +29,7 @@ export default (tracker) => {
router.post("/toggle-freeleech/:infoHash", toggleFreeleech);
router.post("/bookmark/:infoHash", toggleBookmark);
router.get("/latest", listLatest(tracker));
+ router.get("/all", listAll);
router.get("/search", searchTorrents(tracker));
return router;
};
diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js
index a27a2bf..8b89282 100644
--- a/api/src/utils/validateConfig.js
+++ b/api/src/utils/validateConfig.js
@@ -38,6 +38,7 @@ const configSchema = yup
}, {});
return yup.object(entries).required();
}),
+ SQ_ALLOW_UNREGISTERED_VIEW: yup.boolean().required(),
SQ_BASE_URL: yup.string().matches(httpRegex).required(),
SQ_API_URL: yup.string().matches(httpRegex).required(),
SQ_MONGO_URL: yup.string().matches(mongoRegex).required(),
diff --git a/client/pages/index.js b/client/pages/index.js
index 9fb4cbd..dd504e8 100644
--- a/client/pages/index.js
+++ b/client/pages/index.js
@@ -188,6 +188,7 @@ export const getServerSideProps = withAuthServerSideProps(
return { props: {} };
}
},
+ false,
true
);
diff --git a/client/pages/sitemap.xml.js b/client/pages/sitemap.xml.js
new file mode 100644
index 0000000..b667930
--- /dev/null
+++ b/client/pages/sitemap.xml.js
@@ -0,0 +1,56 @@
+import getConfig from "next/config";
+
+const Sitemap = () => {};
+
+export const getServerSideProps = async ({ req, res }) => {
+ const {
+ publicRuntimeConfig: {
+ SQ_BASE_URL,
+ SQ_API_URL,
+ SQ_ALLOW_UNREGISTERED_VIEW,
+ },
+ serverRuntimeConfig: { SQ_SERVER_SECRET },
+ } = getConfig();
+
+ const urls = [SQ_BASE_URL, `${SQ_BASE_URL}/login`, `${SQ_BASE_URL}/register`];
+
+ if (SQ_ALLOW_UNREGISTERED_VIEW) {
+ try {
+ const listRes = await fetch(`${SQ_API_URL}/torrent/all`, {
+ headers: {
+ "Content-Type": "application/json",
+ "X-Forwarded-For":
+ req.headers["x-forwarded-for"] ?? req.socket.remoteAddress,
+ "X-Sq-Server-Secret": SQ_SERVER_SECRET,
+ "X-Sq-Public-Access": true,
+ },
+ });
+ const torrents = await listRes.json();
+ for (const { infoHash } of torrents) {
+ urls.push(`${SQ_BASE_URL}/torrent/${infoHash}`);
+ }
+ } catch (e) {
+ console.error(`[sq] could not list torrents: ${e}`);
+ }
+ }
+
+ const sitemap = `
+
+ ${urls
+ .map(
+ (url) => `
+ ${url}
+ `
+ )
+ .join("\n")}
+
+`;
+
+ res.setHeader("Content-Type", "text/xml");
+ res.write(sitemap);
+ res.end();
+
+ return { props: {} };
+};
+
+export default Sitemap;
diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js
index 001f116..3c9832d 100644
--- a/client/pages/torrent/[infoHash].js
+++ b/client/pages/torrent/[infoHash].js
@@ -126,7 +126,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => {
const [comments, setComments] = useState(torrent.comments);
const [isFreeleech, setIsFreeleech] = useState(torrent.freeleech);
const [hasGroup, setHasGroup] = useState(!!torrent.group);
- const [bookmarked, setBookmarked] = useState(torrent.fetchedBy.bookmarked);
+ const [bookmarked, setBookmarked] = useState(torrent.fetchedBy?.bookmarked);
const { addNotification } = useContext(NotificationContext);
const { setLoading } = useContext(LoadingContext);
@@ -477,9 +477,20 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => {
)}
-
+ {!!userId && (
+
+ )}
{(userRole === "admin" || userId === torrent.uploadedBy._id) && (
<>
)}
-
+ {!!userId ? (
+
+ ) : (
+
+
+
+ )}
{
display="flex"
borderBottom="1px solid"
borderColor="border"
- pb={5}
+ pb={userId ? 5 : 0}
mb={5}
>
-
-
-
+ {!!userId && (
+ <>
+
+
+
+ >
+ )}
{
Remove this torrent
)}
-
-
-
+ {!!userId && (
+
+
+
+ )}
{torrent.groupTorrents.length && hasGroup ? (
@@ -693,15 +726,16 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => {
Comments
-
@@ -780,15 +814,23 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => {
};
export const getServerSideProps = withAuthServerSideProps(
- async ({ token, userId, fetchHeaders, query: { infoHash } }) => {
- if (!token) return { props: {} };
+ async ({
+ token,
+ userId,
+ fetchHeaders,
+ isPublicAccess,
+ query: { infoHash },
+ }) => {
+ if (!token && !isPublicAccess) return { props: {} };
const {
publicRuntimeConfig: { SQ_API_URL },
serverRuntimeConfig: { SQ_JWT_SECRET },
} = getConfig();
- const { id, role } = jwt.verify(token, SQ_JWT_SECRET);
+ const { id, role } = token
+ ? jwt.verify(token, SQ_JWT_SECRET)
+ : { id: null, role: null };
try {
const torrentRes = await fetch(`${SQ_API_URL}/torrent/info/${infoHash}`, {
@@ -810,7 +852,8 @@ export const getServerSideProps = withAuthServerSideProps(
if (e === "banned") throw "banned";
return { props: {} };
}
- }
+ },
+ true
);
export default Torrent;
diff --git a/client/utils/withAuth.js b/client/utils/withAuth.js
index 4ab861e..41f9eba 100644
--- a/client/utils/withAuth.js
+++ b/client/utils/withAuth.js
@@ -30,12 +30,20 @@ export const withAuth = (Component, noRedirect = false) => {
export const withAuthServerSideProps = (
getServerSideProps,
+ publicAccess = false,
noRedirect = false
) => {
return async (ctx) => {
- const { token, userId } = getReqCookies(ctx.req);
+ let { token, userId } = getReqCookies(ctx.req);
- if (!token && !noRedirect)
+ const {
+ serverRuntimeConfig: { SQ_SERVER_SECRET },
+ publicRuntimeConfig: { SQ_ALLOW_UNREGISTERED_VIEW },
+ } = getConfig();
+
+ const isPublicAccess = publicAccess && SQ_ALLOW_UNREGISTERED_VIEW && !token;
+
+ if (!token && !noRedirect && !isPublicAccess)
return {
redirect: {
permanent: false,
@@ -43,26 +51,32 @@ export const withAuthServerSideProps = (
},
};
- if (!token && noRedirect) return { props: {} };
+ if (!token && noRedirect && !isPublicAccess) return { props: {} };
- try {
- const {
- serverRuntimeConfig: { SQ_SERVER_SECRET },
- } = getConfig();
+ if (isPublicAccess) {
+ token = null;
+ userId = null;
+ }
+ try {
const fetchHeaders = {
"Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
"X-Forwarded-For":
ctx.req.headers["x-forwarded-for"] ?? ctx.req.socket.remoteAddress,
"X-Sq-Server-Secret": SQ_SERVER_SECRET,
+ "X-Sq-Public-Access": isPublicAccess,
};
+ if (token) {
+ fetchHeaders["Authorization"] = `Bearer ${token}`;
+ }
+
const { props: ssProps } = await getServerSideProps({
...ctx,
token,
userId,
fetchHeaders,
+ isPublicAccess,
});
return { props: { ...ssProps, token } };
} catch (e) {
diff --git a/config.example.js b/config.example.js
index e8b9c89..e0432aa 100644
--- a/config.example.js
+++ b/config.example.js
@@ -21,6 +21,7 @@ module.exports = {
SQ_BP_COST_PER_INVITE: 3,
SQ_BP_COST_PER_GB: 3,
SQ_SITE_WIDE_FREELEECH: false,
+ SQ_ALLOW_UNREGISTERED_VIEW: false,
SQ_BASE_URL: "https://sqtracker.dev",
SQ_API_URL: "https://sqtracker.dev/api",
SQ_MONGO_URL: "mongodb://sq_mongodb",