diff --git a/components/navigation/NavigationBar.vue b/components/navigation/NavigationBar.vue
index e330ae38..a3771b80 100644
--- a/components/navigation/NavigationBar.vue
+++ b/components/navigation/NavigationBar.vue
@@ -47,7 +47,7 @@
- -
+
-
Admin Settings
- Logout {{ user.username }}
diff --git a/composables/states.ts b/composables/states.ts
index ca433a6a..13e4b44e 100644
--- a/composables/states.ts
+++ b/composables/states.ts
@@ -1,7 +1,7 @@
import { PublicSettings, Category, TokenResponse, TorrentTag } from "torrust-index-types-lib";
import { Rest } from "torrust-index-api-lib";
-import { useRuntimeConfig, useState } from "#app";
import { notify } from "notiwind-ts";
+import { useRuntimeConfig, useState } from "#app";
export const useRestApi = () => useState("rest-api", () => new Rest(useRuntimeConfig().public.apiBase));
export const useCategories = () => useState>("categories", () => new Array());
diff --git a/cypress.config.ts b/cypress.config.ts
index 0eeb983a..4f267f6a 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -1,10 +1,25 @@
import { defineConfig } from "cypress";
+import { grantAdminRole } from "./cypress/e2e/contexts/user/tasks";
+import { DatabaseConfig } from "./cypress/e2e/common/database";
+
+function databaseConfig (config: Cypress.PluginConfigOptions): DatabaseConfig {
+ return {
+ filepath: config.env.db_file_path
+ };
+}
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
setupNodeEvents (on, config) {
- // implement node event listeners here
+ on("task", {
+ grantAdminRole: ({ username }) => {
+ return grantAdminRole(username, databaseConfig(config));
+ }
+ });
}
+ },
+ env: {
+ db_file_path: "./storage/database/torrust_index_backend_e2e_testing.db"
}
});
diff --git a/cypress/e2e/common/database.ts b/cypress/e2e/common/database.ts
new file mode 100644
index 00000000..cae0fc43
--- /dev/null
+++ b/cypress/e2e/common/database.ts
@@ -0,0 +1,33 @@
+import { Database } from "sqlite3";
+
+export interface DatabaseConfig {
+ filepath: string; // Relative path from project root to the SQLite database file
+}
+export interface DatabaseQuery {
+ query: string;
+ params: Array;
+}
+
+export const runDatabaseQuery = ({ query, params }: DatabaseQuery, config: DatabaseConfig): Promise => {
+ return new Promise((resolve, reject) => {
+ const db = new Database(config.filepath, (err) => {
+ if (err) {
+ reject(err.message);
+ }
+ });
+
+ db.get(query, params, function (err, row) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(row);
+ }
+ });
+
+ db.close((err) => {
+ if (err) {
+ reject(err);
+ }
+ });
+ });
+};
diff --git a/cypress/e2e/contexts/user/commands.ts b/cypress/e2e/contexts/user/commands.ts
new file mode 100644
index 00000000..0f9c2703
--- /dev/null
+++ b/cypress/e2e/contexts/user/commands.ts
@@ -0,0 +1,35 @@
+// Custom commands for user context
+
+// Registration
+
+Cypress.Commands.add("register", (registration_form) => {
+ cy.visit("/signup");
+
+ cy.get("input[data-cy=\"registration-form-username\"]").type(registration_form.username);
+ cy.get("input[data-cy=\"registration-form-email\"]").type(registration_form.email);
+ cy.get("input[data-cy=\"registration-form-password\"]").type(registration_form.password);
+ cy.get("input[data-cy=\"registration-form-confirm-password\"]").type(registration_form.confirm_password);
+
+ cy.get("button[data-cy=\"registration-form-submit\"]").click();
+
+ cy.contains("Your account was registered!");
+});
+
+Cypress.Commands.add("register_as_admin", (registration_form) => {
+ cy.register(registration_form);
+
+ cy.task("grantAdminRole", { username: registration_form.username });
+});
+
+// Authentication
+
+Cypress.Commands.add("login", (username: string, password: string) => {
+ cy.visit("/signin");
+
+ cy.get("input[data-cy=\"login-form-username\"]").type(username);
+ cy.get("input[data-cy=\"login-form-password\"]").type(password);
+
+ cy.get("button[data-cy=\"login-form-submit\"]").click();
+
+ cy.url().should("include", "/torrents");
+});
diff --git a/cypress/e2e/contexts/user/registration.ts b/cypress/e2e/contexts/user/registration.ts
new file mode 100644
index 00000000..0ab1cdac
--- /dev/null
+++ b/cypress/e2e/contexts/user/registration.ts
@@ -0,0 +1,19 @@
+export type RegistrationForm = {
+ username: string
+ email: string
+ password: string
+ confirm_password: string
+}
+
+export function random_user_registration_data (): RegistrationForm {
+ return {
+ username: `user${random_user_id()}`,
+ email: `user${random_user_id()}@example.com`,
+ password: "12345678",
+ confirm_password: "12345678"
+ };
+}
+
+function random_user_id (): number {
+ return Math.floor(Math.random() * 1000000);
+}
diff --git a/cypress/e2e/contexts/user/specs/authentication.cy.ts b/cypress/e2e/contexts/user/specs/authentication.cy.ts
new file mode 100644
index 00000000..c6a72adf
--- /dev/null
+++ b/cypress/e2e/contexts/user/specs/authentication.cy.ts
@@ -0,0 +1,41 @@
+import { random_user_registration_data } from "../registration";
+
+describe("A registered user", () => {
+ beforeEach(() => {
+ cy.visit("/");
+ });
+
+ it("should be able to sign in", () => {
+ cy.visit("/signup");
+
+ const registration_form = random_user_registration_data();
+
+ cy.register(registration_form);
+
+ cy.visit("/signin");
+
+ cy.get("input[data-cy=\"login-form-username\"]").type(registration_form.username);
+ cy.get("input[data-cy=\"login-form-password\"]").type(registration_form.password);
+
+ cy.get("button[data-cy=\"login-form-submit\"]").click();
+
+ cy.url().should("include", "/torrents");
+ });
+});
+
+describe("The website admin", () => {
+ beforeEach(() => {
+ cy.visit("/");
+ });
+
+ it("should be able to sign in as admin", () => {
+ const registration_form = random_user_registration_data();
+
+ cy.register_as_admin(registration_form);
+
+ cy.login(registration_form.username, registration_form.password);
+
+ // If the user is an admin, the link to admin settings should be available
+ cy.get("li[data-cy=\"admin-settings-link\"]");
+ });
+});
diff --git a/cypress/e2e/contexts/user/registration.cy.ts b/cypress/e2e/contexts/user/specs/registration.cy.ts
similarity index 51%
rename from cypress/e2e/contexts/user/registration.cy.ts
rename to cypress/e2e/contexts/user/specs/registration.cy.ts
index 53be269b..bc314fbe 100644
--- a/cypress/e2e/contexts/user/registration.cy.ts
+++ b/cypress/e2e/contexts/user/specs/registration.cy.ts
@@ -1,22 +1,4 @@
-type RegistrationForm = {
- username: string
- email: string
- password: string
- confirm_password: string
-}
-
-function random_user_registration_form (): RegistrationForm {
- return {
- username: `user${random_user_id()}`,
- email: `user${random_user_id()}@example.com`,
- password: "12345678",
- confirm_password: "12345678"
- };
-}
-
-function random_user_id (): number {
- return Math.floor(Math.random() * 1000000);
-}
+import { random_user_registration_data } from "../registration";
describe("A guest", () => {
beforeEach(() => {
@@ -24,12 +6,10 @@ describe("A guest", () => {
});
it("should be able to sign up", () => {
- cy.visit("/signup");
+ const registration_form = random_user_registration_data();
- const registration_form = random_user_registration_form();
+ cy.visit("/signup");
- // See Cypress Docs -> Best Practices -> Selecting Elements
- // https://docs.cypress.io/guides/references/best-practices#Selecting-Elements
cy.get("input[data-cy=\"registration-form-username\"]").type(registration_form.username);
cy.get("input[data-cy=\"registration-form-email\"]").type(registration_form.email);
cy.get("input[data-cy=\"registration-form-password\"]").type(registration_form.password);
diff --git a/cypress/e2e/contexts/user/tasks.ts b/cypress/e2e/contexts/user/tasks.ts
new file mode 100644
index 00000000..80037f1d
--- /dev/null
+++ b/cypress/e2e/contexts/user/tasks.ts
@@ -0,0 +1,36 @@
+// Custom tasks for user context
+
+import { DatabaseConfig, DatabaseQuery, runDatabaseQuery } from "../../common/database";
+
+// Task to grant admin role to a user by username
+export const grantAdminRole = async (username: string, db_config: DatabaseConfig): Promise => {
+ let user_id: number;
+
+ try {
+ const result = await runDatabaseQuery(getUserIdByUsernameQuery(username), db_config);
+
+ const user_id = result.user_id;
+
+ await runDatabaseQuery(grantAdminRoleToUserWithId(user_id), db_config);
+
+ return user_id;
+ } catch (err) {
+ return await Promise.reject(err);
+ }
+};
+
+// Database query specifications
+
+function getUserIdByUsernameQuery (username: string): DatabaseQuery {
+ return {
+ query: "SELECT user_id FROM torrust_user_profiles WHERE username = ?",
+ params: [username]
+ };
+}
+
+function grantAdminRoleToUserWithId (user_id: number): DatabaseQuery {
+ return {
+ query: "UPDATE torrust_users SET administrator = ? WHERE user_id = ?",
+ params: [true, user_id]
+ };
+}
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 95857aea..26fc315e 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -1,37 +1,14 @@
-///
-// ***********************************************
-// This example commands.ts shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add('login', (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-//
-// declare global {
-// namespace Cypress {
-// interface Chainable {
-// login(email: string, password: string): Chainable
-// drag(subject: string, options?: Partial): Chainable
-// dismiss(subject: string, options?: Partial): Chainable
-// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
-// }
-// }
-// }
+import "../e2e/contexts/user/commands";
+import { RegistrationForm } from "../e2e/contexts/user/registration";
+
+declare global {
+ namespace Cypress {
+ interface Chainable {
+ // Registration
+ register(registration_form: RegistrationForm): Chainable
+ register_as_admin(registration_form: RegistrationForm): Chainable
+ // Authentication
+ login(username: string, password: string): Chainable
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 7584aac5..1cca95ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"@types/dompurify": "^3.0.2",
"@types/marked": "^5.0.0",
"@types/node": "^20.4.1",
+ "@types/sqlite3": "^3.1.8",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"cypress": "^12.17.0",
@@ -30,6 +31,7 @@
"i": "^0.3.7",
"npm": "^9.8.0",
"nuxt": "^3.6.2",
+ "sqlite3": "^5.1.6",
"typescript": "^5.1.6",
"vite-plugin-eslint": "^1.8.1"
}
@@ -1590,6 +1592,13 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "dev": true,
+ "optional": true
+ },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -1993,6 +2002,34 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/@npmcli/move-file": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
+ "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@npmcli/node-gyp": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz",
@@ -2912,6 +2949,15 @@
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
+ "node_modules/@types/sqlite3": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.8.tgz",
+ "integrity": "sha512-sQMt/qnyUWnqiTcJXm5ZfNPIBeJ/DVvJDwxw+0tAxPJvadzfiP1QhryO1JOR6t1yfb8NpzQb/Rud06mob5laIA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
@@ -8882,6 +8928,13 @@
"node": ">=8"
}
},
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "dev": true,
+ "optional": true
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -11101,6 +11154,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/node-addon-api": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
+ "dev": true
+ },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -17659,6 +17718,409 @@
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==",
"dev": true
},
+ "node_modules/sqlite3": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz",
+ "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.0",
+ "node-addon-api": "^4.2.0",
+ "tar": "^6.1.11"
+ },
+ "optionalDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependenciesMeta": {
+ "node-gyp": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sqlite3/node_modules/@npmcli/fs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
+ "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@gar/promisify": "^1.0.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/sqlite3/node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sqlite3/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/cacache": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
+ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@npmcli/fs": "^1.0.0",
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.1",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/sqlite3/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/sqlite3/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/sqlite3/node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sqlite3/node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sqlite3/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sqlite3/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sqlite3/node_modules/make-fetch-happen": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
+ "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "agentkeepalive": "^4.1.3",
+ "cacache": "^15.2.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.3",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^1.3.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.2",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^6.0.0",
+ "ssri": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/sqlite3/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sqlite3/node_modules/minipass-fetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
+ "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.0",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.12"
+ }
+ },
+ "node_modules/sqlite3/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sqlite3/node_modules/node-gyp": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
+ "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^9.1.0",
+ "nopt": "^5.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/socks-proxy-agent": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
+ "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/sqlite3/node_modules/ssri": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
+ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/sqlite3/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sqlite3/node_modules/unique-filename": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
+ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "unique-slug": "^2.0.0"
+ }
+ },
+ "node_modules/sqlite3/node_modules/unique-slug": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz",
+ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ }
+ },
+ "node_modules/sqlite3/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/sqlite3/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "optional": true
+ },
"node_modules/sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
diff --git a/package.json b/package.json
index 3167ee33..048c550a 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@types/dompurify": "^3.0.2",
"@types/marked": "^5.0.0",
"@types/node": "^20.4.1",
+ "@types/sqlite3": "^3.1.8",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"cypress": "^12.17.0",
@@ -27,6 +28,7 @@
"i": "^0.3.7",
"npm": "^9.8.0",
"nuxt": "^3.6.2",
+ "sqlite3": "^5.1.6",
"typescript": "^5.1.6",
"vite-plugin-eslint": "^1.8.1"
},
diff --git a/pages/torrent/[infoHash].vue b/pages/torrent/[infoHash].vue
index e2ad0e60..c59c96d7 100644
--- a/pages/torrent/[infoHash].vue
+++ b/pages/torrent/[infoHash].vue
@@ -41,8 +41,8 @@
import { ChevronLeftIcon } from "@heroicons/vue/24/solid";
import { Ref } from "vue";
import { TorrentResponse } from "torrust-index-types-lib";
-import { useRoute, useRuntimeConfig } from "#app";
import { notify } from "notiwind-ts";
+import { useRoute, useRuntimeConfig } from "#app";
import TorrentActionCard from "~/components/torrent/TorrentActionCard.vue";
import TorrentDescriptionTab from "~/components/torrent/TorrentDescriptionTab.vue";
import TorrentFilesTab from "~/components/torrent/TorrentFilesTab.vue";