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/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/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 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 ( 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" }