From 3fe0d446987a42d422cfac5535ffe61bfa97b768 Mon Sep 17 00:00:00 2001 From: enjarai Date: Thu, 20 Mar 2025 23:24:50 +0100 Subject: [PATCH 1/3] Hm. Meow. --- docker-compose.dev.yml | 94 ++++++++++++++++++++++-------------------- docker-compose.yml | 87 ++++++++++++++++++++++---------------- package.json | 3 +- 3 files changed, 102 insertions(+), 82 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c676757..8c68160 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,22 +1,22 @@ version: "3.9" services: - traefik: - image: "traefik:v2.5" - container_name: "sq_traefik" - command: - - "--api.insecure=true" - - "--providers.file=true" - - "--providers.file.filename=/config/traefik.yml" - - "--entrypoints.web.address=:80" - - "--entryPoints.web.proxyProtocol.insecure" - - "--entryPoints.web.forwardedHeaders.insecure" - restart: always - ports: - - "80:80" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - ./traefik.yml:/config/traefik.yml +# traefik: +# image: "traefik:v2.5" +# container_name: "sq_traefik" +# command: +# - "--api.insecure=true" +# - "--providers.file=true" +# - "--providers.file.filename=/config/traefik.yml" +# - "--entrypoints.web.address=:80" +# - "--entryPoints.web.proxyProtocol.insecure" +# - "--entryPoints.web.forwardedHeaders.insecure" +# restart: always +# ports: +# - "80:80" +# - "8080:8080" +# volumes: +# - "/var/run/docker.sock:/var/run/docker.sock:ro" +# - ./traefik.yml:/config/traefik.yml # nginx: # image: "nginx:latest" # container_name: "sq_nginx" @@ -28,34 +28,38 @@ services: database: container_name: sq_mongodb image: mongo:6.0 - volumes: - - ./data:/data/db - api: - container_name: sq_api - build: - context: ./api - dockerfile: Dockerfile - restart: always ports: - - "127.0.0.1:3001:3001" + - "127.0.0.1:27017:27017" volumes: - - type: bind - source: ./config.js - target: /sqtracker/config.js - depends_on: - - database - client: - container_name: sq_client - build: - context: ./client - dockerfile: Dockerfile - restart: always - ports: - - "127.0.0.1:3000:3000" - volumes: - - type: bind - source: ./config.js - target: /sqtracker/config.js - depends_on: - - api + - db:/data/db +# api: +# container_name: sq_api +# build: +# context: ./api +# dockerfile: Dockerfile +# restart: always +# ports: +# - "127.0.0.1:3001:3001" +# volumes: +# - type: bind +# source: ./config.js +# target: /sqtracker/config.js +# depends_on: +# - database +# client: +# container_name: sq_client +# build: +# context: ./client +# dockerfile: Dockerfile +# restart: always +# ports: +# - "127.0.0.1:3000:3000" +# volumes: +# - type: bind +# source: ./config.js +# target: /sqtracker/config.js +# depends_on: +# - api +volumes: + db: diff --git a/docker-compose.yml b/docker-compose.yml index ca4737a..3e4a709 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,48 +1,55 @@ -version: "3.9" +#version: "3.9" services: - traefik: - image: "traefik:v2.5" - container_name: "sq_traefik" - command: - - "--api.insecure=true" - - "--providers.file=true" - - "--providers.file.filename=/config/traefik.yml" - - "--entrypoints.webinsecure.address=:80" - - "--entrypoints.web.address=:443" - - "--entryPoints.web.proxyProtocol.insecure" - - "--entryPoints.web.forwardedHeaders.insecure" - - "--certificatesresolvers.tlsresolver.acme.email=email@example.com" - - "--certificatesresolvers.tlsresolver.acme.storage=/letsencrypt/acme.json" - - "--certificatesresolvers.tlsresolver.acme.httpchallenge=true" - - "--certificatesresolvers.tlsresolver.acme.httpchallenge.entrypoint=webinsecure" - ports: - - "80:80" - - "443:443" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - ./letsencrypt:/letsencrypt - - ./traefik.yml:/config/traefik.yml -# nginx: -# image: "nginx:latest" -# container_name: "sq_nginx" -# restart: always -# ports: -# - "80:80" -# volumes: -# - ./nginx.conf:/etc/nginx/nginx.conf + # traefik: + # image: "traefik:v2.5" + # container_name: "sq_traefik" + # command: + # - "--api.insecure=true" + # - "--providers.file=true" + # - "--providers.file.filename=/config/traefik.yml" + # - "--entrypoints.webinsecure.address=:80" + # - "--entrypoints.web.address=:443" + # - "--entryPoints.web.proxyProtocol.insecure" + # - "--entryPoints.web.forwardedHeaders.insecure" + # - "--certificatesresolvers.tlsresolver.acme.email=email@example.com" + # - "--certificatesresolvers.tlsresolver.acme.storage=/letsencrypt/acme.json" + # - "--certificatesresolvers.tlsresolver.acme.httpchallenge=true" + # - "--certificatesresolvers.tlsresolver.acme.httpchallenge.entrypoint=webinsecure" + # ports: + # - "80:80" + # - "443:443" + # - "8080:8080" + # volumes: + # - "/var/run/docker.sock:/var/run/docker.sock:ro" + # - ./letsencrypt:/letsencrypt + # - ./traefik.yml:/config/traefik.yml + # nginx: + # image: "nginx:latest" + # container_name: "sq_nginx" + # restart: always + # ports: + # - "80:80" + # volumes: + # - ./nginx.conf:/etc/nginx/nginx.conf database: container_name: sq_mongodb image: mongo:6.0 - ports: - - "127.0.0.1:27017:27017" + # ports: + # - "127.0.0.1:27017:27017" + networks: + - sqtracker volumes: - ./data:/data/db api: container_name: sq_api - image: ghcr.io/tdjsnelling/sqtracker-api:latest + # image: ghcr.io/tdjsnelling/sqtracker-api:latest + build: + context: ./api/ + dockerfile: Dockerfile ports: - "127.0.0.1:3001:3001" + networks: + - sqtracker volumes: - type: bind source: ./config.js @@ -51,9 +58,14 @@ services: - database client: container_name: sq_client - image: ghcr.io/tdjsnelling/sqtracker-client:latest + # image: ghcr.io/tdjsnelling/sqtracker-client:latest + build: + context: ./client/ + dockerfile: Dockerfile ports: - "127.0.0.1:3000:3000" + networks: + - sqtracker volumes: - type: bind source: ./config.js @@ -62,3 +74,6 @@ services: # - ./favicon.ico:/sqtracker/public/favicon.ico depends_on: - api + +networks: + sqtracker: diff --git a/package.json b/package.json index 64a56f4..d9ff313 100644 --- a/package.json +++ b/package.json @@ -16,5 +16,6 @@ "devDependencies": { "npm-run-all": "^4.1.5" }, - "dependencies": {} + "dependencies": {}, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } From 24a7307981468d92811b8248ccf4db61ef0b30e9 Mon Sep 17 00:00:00 2001 From: enjarai Date: Thu, 20 Mar 2025 23:44:41 +0100 Subject: [PATCH 2/3] Hm. Meow. 2 --- api/src/controllers/torrent.js | 1 + api/src/middleware/auth.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index e82bdc2..59e64e9 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -509,6 +509,7 @@ export const getTorrentsPage = async ({ freeleech: 1, tags: 1, confidenceScore: 1, + size: 1, }, }, ...(Array.isArray(ids) diff --git a/api/src/middleware/auth.js b/api/src/middleware/auth.js index 4da4b86..ce620b9 100644 --- a/api/src/middleware/auth.js +++ b/api/src/middleware/auth.js @@ -25,6 +25,9 @@ const auth = async (req, res, next) => { } catch (err) { res.status(500).send(err); } + // } else if (req.headers.username && req.headers.password) { + // const user = await User.findOne({ username: req.headers.username }).lean(); + // } else if ( req.headers["x-sq-public-access"] === "true" && req.headers["x-sq-server-secret"] === process.env.SQ_SERVER_SECRET From bf257c9c1d50c93b9cfe5c160c6f87d50b47d40a Mon Sep 17 00:00:00 2001 From: enjarai Date: Fri, 21 Mar 2025 00:36:49 +0100 Subject: [PATCH 3/3] Hm. Meow. 3 --- api/src/controllers/user.js | 70 ++++++++++++++++++++++ api/src/routes/user.js | 4 +- client/locales/en.json | 6 ++ client/pages/account.js | 10 +++- client/pages/user/[username].js | 102 +++++++++++++++++++++++++------- 5 files changed, 168 insertions(+), 24 deletions(-) diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 7fba1b5..d74b2c3 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -803,6 +803,30 @@ export const banUser = async (req, res, next) => { } }; +export const promoteUser = async (req, res, next) => { + try { + if (req.userRole !== "admin") { + res.status(401).send("You do not have permission to promote a user"); + return; + } + + const user = await User.findOne({ username: req.params.username }); + if (!user) { + res.status(404).send("User does not exist"); + return; + } + + await User.findOneAndUpdate( + { username: req.params.username }, + { $set: { role: "admin" } } + ); + + res.sendStatus(200); + } catch (e) { + next(e); + } +}; + export const buyItems = async (req, res, next) => { if (req.body.type && req.body.amount) { try { @@ -875,6 +899,23 @@ export const buyItems = async (req, res, next) => { await progressRecord.save(); + res.status(200).send((user.bonusPoints - cost).toString()); + } else if (req.body.type === "headpat") { + const cost = amount; + if (cost > user.bonusPoints) { + res.status(403).send("Not enough points for transaction"); + return; + } + + await User.findOneAndUpdate( + { _id: req.userId }, + { + $inc: { + bonusPoints: cost * -1, + }, + } + ); + res.status(200).send((user.bonusPoints - cost).toString()); } else { res.status(400).send("Type must be one of invite, upload"); @@ -912,6 +953,35 @@ export const unbanUser = async (req, res, next) => { } }; +export const demoteUser = async (req, res, next) => { + try { + if (req.userRole !== "admin" || req.params.username === "admin") { + res.status(401).send("You do not have permission to demote a user"); + return; + } + + const user = await User.findOne({ username: req.params.username }); + if (!user) { + res.status(404).send("User does not exist"); + return; + } + + if (req.userId === user.id) { + res.status(401).send("You cannot demote yourself"); + return; + } + + await User.findOneAndUpdate( + { username: req.params.username }, + { $set: { role: "user" } } + ); + + res.sendStatus(200); + } catch (e) { + next(e); + } +}; + export const generateTotpSecret = async (req, res, next) => { try { const user = await User.findOne({ _id: req.userId }).lean(); diff --git a/api/src/routes/user.js b/api/src/routes/user.js index 7f7367c..206d395 100644 --- a/api/src/routes/user.js +++ b/api/src/routes/user.js @@ -1,5 +1,5 @@ import express from "express"; -import { banUser, fetchUser, unbanUser } from "../controllers/user"; +import { banUser, demoteUser, fetchUser, promoteUser, unbanUser } from "../controllers/user"; const router = express.Router(); @@ -7,5 +7,7 @@ export default (tracker) => { router.get("/:username", fetchUser(tracker)); router.post("/ban/:username", banUser); router.post("/unban/:username", unbanUser); + router.post("/promote/:username", promoteUser); + router.post("/demote/:username", demoteUser); return router; }; diff --git a/client/locales/en.json b/client/locales/en.json index 646f16b..f9224d6 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -42,10 +42,12 @@ "accInviteText1": "Enter an email address to send an invite. The invited user will need to sign up with the same email address. Once the invite is generated, you can also copy a direct invite link.", "accInviteText1NoEmail": "Enter an email address to generate an invite. The invited user will need to sign up with the same email address. This tracker has email sending disabled, so you will need to copy and send the invite link yourself.", "accItemsPurchasedSuccess": "Items purchased successfully", + "getPet": "*petting you* Good girl!", "accMyAccount": "My account", "accPassChangedSuccess": "Password changed successfully", "accPurchaseInvites": "Purchase invites", "accPurchaseUpload1GB": "Purchase upload (1 GB)", + "accPurchaseHeadpat": "Purchase headpat", "accRole": "Role", "accSendInvite": "Send invite", "accSendInviteNoEmail": "Create invite", @@ -286,6 +288,10 @@ "userBan": "Ban", "userUnbanned": "unbanned", "userBanned": "banned", + "userDemote": "Demote", + "userPromote": "Promote", + "userDemoted": "demoted", + "userPromoted": "promoted", "userAdmin": "Admin", "veCouldNotVerifyEmailAddress": "Could not verify email address:", "veEmailAddressVerifiedSuccess": "Email address verified successfully.", diff --git a/client/pages/account.js b/client/pages/account.js index 8335c0e..af3fb89 100644 --- a/client/pages/account.js +++ b/client/pages/account.js @@ -228,7 +228,9 @@ const Account = ({ token, invites = [], user, userRole }) => { addNotification( "success", - `${getLocaleString("accItemsPurchasedSuccess")}` + type === "headpat" + ? `${getLocaleString("getPet")}` + : `${getLocaleString("accItemsPurchasedSuccess")}` ); const pointsRemaining = await buyRes.text(); @@ -407,6 +409,12 @@ const Account = ({ token, invites = [], user, userRole }) => { wallet={bonusPoints} handleBuy={(e) => handleBuy(e, "upload")} /> + handleBuy(e, "headpat")} + /> {(SQ_ALLOW_REGISTER === "invite" || userRole === "admin") && ( <> diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index 25d0ef4..927fcc8 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -26,6 +26,7 @@ import LocaleContext from "../../utils/LocaleContext"; const User = ({ token, user, userRole }) => { const [banned, setBanned] = useState(!!user.banned); + const [role, setRole] = useState(user.role); const [showBanModal, setShowBanModal] = useState(false); const { addNotification } = useContext(NotificationContext); @@ -38,8 +39,8 @@ const User = ({ token, user, userRole }) => { SQ_TORRENT_CATEGORIES, SQ_MINIMUM_RATIO, SQ_MAXIMUM_HIT_N_RUNS, - SQ_API_URL, - }, + SQ_API_URL + } } = getConfig(); const downloadedBytes = prettyBytes(user.downloaded?.bytes || 0).split(" "); @@ -56,8 +57,8 @@ const User = ({ token, user, userRole }) => { { method: "POST", headers: { - Authorization: `Bearer ${token}`, - }, + Authorization: `Bearer ${token}` + } } ); @@ -90,6 +91,53 @@ const User = ({ token, user, userRole }) => { setLoading(false); }; + const handlePromoteUser = async () => { + setLoading(true); + + try { + const res = await fetch( + `${SQ_API_URL}/user/${role === "user" ? "promote" : "demote"}/${ + user.username + }`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}` + } + } + ); + + if (res.status !== 200) { + const reason = await res.text(); + throw new Error(reason); + } + + addNotification( + "success", + `${user.username} ${ + role !== "user" + ? [getLocaleString("userDemoted")] + : [getLocaleString("userPromoted")] + } ${getLocaleString("userSuccessfully")}` + ); + + setRole((r) => (r === "user" ? "admin" : "user")); + // setShowBanModal(false); + } catch (e) { + addNotification( + "error", + `${getLocaleString("userCouldNot")} ${ + role !== "user" + ? [getLocaleString("userDemoted")] + : [getLocaleString("userPromoted")] + } ${user.username}: ${e.message}` + ); + console.error(e); + } + + setLoading(false); + }; + const cards = useMemo(() => { let c = 2; if (SQ_MINIMUM_RATIO !== -1) c++; @@ -110,7 +158,7 @@ const User = ({ token, user, userRole }) => { {user.username} {getLocaleString("userProfile")} - {user.role === "admin" && ( + {role === "admin" && ( {getLocaleString("userAdmin")} @@ -121,21 +169,31 @@ const User = ({ token, user, userRole }) => { )} - {cookies.username === user.username && ( - - - - - - )} - {userRole === "admin" && cookies.username !== user.username && ( - - )} + + {cookies.username === user.username && ( + + + + + + )} + {userRole === "admin" && cookies.username !== user.username && ( + + )} + {userRole === "admin" && cookies.username !== user.username && ( + + )} + {getLocaleString("userUserSince")}{" "} @@ -351,14 +409,14 @@ export const getServerSideProps = withAuthServerSideProps( const { publicRuntimeConfig: { SQ_API_URL }, - serverRuntimeConfig: { SQ_JWT_SECRET }, + serverRuntimeConfig: { SQ_JWT_SECRET } } = getConfig(); const { role } = jwt.verify(token, SQ_JWT_SECRET); try { const userRes = await fetch(`${SQ_API_URL}/user/${username}`, { - headers: fetchHeaders, + headers: fetchHeaders }); if (