diff --git a/README.md b/README.md index 1e279f6..f43e8e3 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,36 @@ npm install 2. Update the `.env` file in `nsc-events-nestjs` with your PostgreSQL credentials 3. The database tables will be automatically created when you start the backend +### Database Seeding + +Once both the backend and the database are running, you can populate the database with sample data for development: + +```bash +# Seed both users and events (recommended — runs users first, then events) +npm run seed:all + +# Seed only sample users (admin, creator, and regular user accounts) +npm run seed:users + +# Seed only sample events (requires the seed creator account to exist) +npm run seed +``` + +The seed scripts connect to the NestJS API (default: `http://localhost:3000/api`). Override the target with the `PLAYWRIGHT_API_URL` environment variable if your backend is on a different port. + +**Seeded accounts** + +| Email | Password | Role | +|-------|----------|------| +| `admin@nsc.dev` | `admin@admin123` | admin | +| `admin2@nsc.dev` | `Admin2@dev456` | admin | +| `seed-creator@nsc.dev` | `Seed@Creator123` | creator | +| `user@nsc.dev` | `User@Alex123` | user | +| `user2@nsc.dev` | `User@Sam456` | user | +| `user3@nsc.dev` | `User@Jordan789` | user | + +> **Note:** These accounts are for local development only. Do not use these credentials in production environments. + ### Running the Applications **Backend:** diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts index 4bd0d79..a964f5a 100644 --- a/e2e/fixtures/auth.ts +++ b/e2e/fixtures/auth.ts @@ -1,6 +1,30 @@ import { test as base, expect } from '@playwright/test'; import { ApiClient } from '../utils/api-client'; +const ADMIN_API_URL = + process.env.PLAYWRIGHT_API_URL || "http://localhost:3000/api"; +const ADMIN_CREDENTIALS = { + email: "admin@nsc.dev", + password: "admin@admin123", +}; + +/** Returns an ApiClient authenticated as the seed admin. Best-effort — returns null if login fails. */ +async function getAdminClient(): Promise { + try { + const adminClient = new ApiClient(ADMIN_API_URL); + const res = await adminClient.login( + ADMIN_CREDENTIALS.email, + ADMIN_CREDENTIALS.password, + ); + const token = res.data?.token || res.data?.data?.token; + if (!token) return null; + adminClient.setToken(token); + return adminClient; + } catch { + return null; + } +} + type AuthFixtures = { authenticatedPage: { page: any; @@ -36,7 +60,10 @@ export const test = base.extend({ ); // Login to get token - const loginResponse = await apiClient.login(testUser.email, testUser.password); + const loginResponse = await apiClient.login( + testUser.email, + testUser.password, + ); const token = loginResponse.data.access_token; apiClient.setToken(token); @@ -44,23 +71,26 @@ export const test = base.extend({ // Set authentication context in page await page.context().addCookies([ { - name: 'next-auth.session-token', + name: "next-auth.session-token", value: token, - domain: 'localhost', - path: '/', + domain: "localhost", + path: "/", httpOnly: true, secure: false, - sameSite: 'Lax', + sameSite: "Lax", }, ]); // Store token in localStorage - await page.goto('/'); - await page.evaluate(({ tokenValue }) => { - if (tokenValue) { - localStorage.setItem('authToken', tokenValue); - } - }, { tokenValue: token }); + await page.goto("/"); + await page.evaluate( + ({ tokenValue }) => { + if (tokenValue) { + localStorage.setItem("authToken", tokenValue); + } + }, + { tokenValue: token }, + ); const user = signupResponse.data.user || loginResponse.data.user; @@ -69,6 +99,24 @@ export const test = base.extend({ apiClient, user, }); + + // ── Cleanup: delete the test user via admin account ────────────────── + try { + const adminClient = await getAdminClient(); + if (adminClient) { + // Resolve user ID (may come from signup or login response) + let userId: string | undefined = user?.id; + if (!userId) { + const found = await adminClient.getUserByEmail(testUser.email); + userId = found.data?.id || found.data?.data?.id; + } + if (userId) { + await adminClient.deleteUser(userId); + } + } + } catch { + // Best-effort — global teardown will sweep any stragglers + } } catch (error) { console.error('Failed to set up authenticated user:', error); throw error; diff --git a/e2e/tests/event-management.spec.ts b/e2e/tests/event-management.spec.ts index 39d22a0..294fb76 100644 --- a/e2e/tests/event-management.spec.ts +++ b/e2e/tests/event-management.spec.ts @@ -62,6 +62,25 @@ test.describe("Event Management", () => { createdEventIds = []; }); + test.afterAll(async () => { + // Delete the test creator user via admin account + if (!userId) return; + try { + const adminClient = new ApiClient(); + const loginRes = await adminClient.login( + "admin@nsc.dev", + "admin@admin123", + ); + const adminToken = loginRes.data?.token || loginRes.data?.data?.token; + if (adminToken) { + adminClient.setToken(adminToken); + await adminClient.deleteUser(userId); + } + } catch { + // Best-effort — global teardown will handle any remaining test users + } + }); + test("should create a new event via UI and redirect to event detail", async ({ page, browserName }) => { // Skip on Mobile Chrome due to MUI DatePicker mobile dialog interaction complexity test.skip(browserName === "chromium" && test.info().project.name === "Mobile Chrome", diff --git a/e2e/tests/event-registration.spec.ts b/e2e/tests/event-registration.spec.ts index 2bc2281..c9083fc 100644 --- a/e2e/tests/event-registration.spec.ts +++ b/e2e/tests/event-registration.spec.ts @@ -26,6 +26,7 @@ async function signupWithRetry(page, data, attempts = 3) { test.describe('Event Registration', () => { let userToken: string; let eventId: string; + let userId: string; test.beforeAll(async ({ browser }) => { // Setup: Create user and event for registration tests @@ -38,6 +39,7 @@ test.describe('Event Registration', () => { if (signupResponse.ok()) { const data = await signupResponse.json(); userToken = data.token; + userId = data.user?.id || data.data?.user?.id; // Create an event const createEventResponse = await page.request.post('http://localhost/api/event-registration', { @@ -63,6 +65,46 @@ test.describe('Event Registration', () => { await page.close(); }); + test.afterAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + // Delete the test event + if (eventId && userToken) { + await page.request.delete( + `http://localhost:3000/api/events/remove/${eventId}`, + { + headers: { Authorization: `Bearer ${userToken}` }, + }, + ); + } + // Delete the test user via admin + if (userId) { + const adminRes = await page.request.post( + "http://localhost:3000/api/auth/login", + { + data: { email: "admin@nsc.dev", password: "admin@admin123" }, + }, + ); + if (adminRes.ok()) { + const adminData = await adminRes.json(); + const adminToken = adminData.token || adminData.data?.token; + if (adminToken) { + await page.request.delete( + `http://localhost:3000/api/users/remove/${userId}`, + { + headers: { Authorization: `Bearer ${adminToken}` }, + }, + ); + } + } + } + } catch { + // Best-effort — global teardown will handle stragglers + } finally { + await page.close(); + } + }); + test('should register for an event', async ({ page }) => { if (!userToken || !eventId) { test.skip(true, 'Test setup failed to create user or event'); diff --git a/e2e/tests/user-management.spec.ts b/e2e/tests/user-management.spec.ts index 9d3b901..9d1f03f 100644 --- a/e2e/tests/user-management.spec.ts +++ b/e2e/tests/user-management.spec.ts @@ -21,6 +21,7 @@ async function setAuthToken(page: Page, token: string): Promise { test.describe('User Management & Admin Functions', () => { let adminToken: string; let testUserId: string; + let testAdminUserId: string; test.beforeAll(async ({ browser }) => { // Setup: Create admin user @@ -38,6 +39,7 @@ test.describe('User Management & Admin Functions', () => { if (signupResponse.ok()) { const data = await signupResponse.json(); adminToken = data.token; + testAdminUserId = data.user?.id || data.data?.user?.id; // Create a regular user for role management tests const regularUser = generateTestUser(); @@ -54,6 +56,43 @@ test.describe('User Management & Admin Functions', () => { await page.close(); }); + test.afterAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + const adminRes = await page.request.post( + "http://localhost:3000/api/auth/login", + { + data: { email: "admin@nsc.dev", password: "admin@admin123" }, + }, + ); + if (adminRes.ok()) { + const adminData = await adminRes.json(); + const seedAdminToken = adminData.token || adminData.data?.token; + if (seedAdminToken) { + const headers = { Authorization: `Bearer ${seedAdminToken}` }; + // Delete regular test user + if (testUserId) { + await page.request.delete( + `http://localhost:3000/api/users/remove/${testUserId}`, + { headers }, + ); + } + // Delete the test admin user created by this suite + if (testAdminUserId) { + await page.request.delete( + `http://localhost:3000/api/users/remove/${testAdminUserId}`, + { headers }, + ); + } + } + } + } catch { + // Best-effort — global teardown will handle stragglers + } finally { + await page.close(); + } + }); + test('should view user profile', async ({ page, browserName }) => { // Skip on Firefox due to CORS issues with API calls in authenticated context test.skip(browserName === "firefox", diff --git a/e2e/utils/api-client.ts b/e2e/utils/api-client.ts index 2b5742b..ff62b10 100644 --- a/e2e/utils/api-client.ts +++ b/e2e/utils/api-client.ts @@ -4,7 +4,9 @@ export class ApiClient { private client: AxiosInstance; private token: string | null = null; - constructor(baseURL: string = process.env.PLAYWRIGHT_API_URL || "http://localhost/api") { + constructor( + baseURL: string = process.env.PLAYWRIGHT_API_URL || "http://localhost/api", + ) { this.client = axios.create({ baseURL, headers: { @@ -28,7 +30,7 @@ export class ApiClient { firstName: string, lastName: string, pronouns: string = "they/them", - role: string = "user" + role: string = "user", ) { return this.client.post("/auth/signup", { email, @@ -91,6 +93,24 @@ export class ApiClient { this.token = null; delete this.client.defaults.headers.common["Authorization"]; } + + // ─── Admin helpers ─────────────────────────────────────────────────────────── + + async getAllUsers() { + return this.client.get("/users"); + } + + async getUserByEmail(email: string) { + return this.client.get(`/users/email/${encodeURIComponent(email)}`); + } + + async deleteUser(id: string) { + return this.client.delete(`/users/remove/${id}`); + } + + async getAllEvents(limit = 500) { + return this.client.get(`/events?numberOfEventsToGet=${limit}`); + } } export const createApiClient = () => new ApiClient(); diff --git a/e2e/utils/global-teardown.ts b/e2e/utils/global-teardown.ts index 7a2a171..24bffed 100644 --- a/e2e/utils/global-teardown.ts +++ b/e2e/utils/global-teardown.ts @@ -1,17 +1,134 @@ import { execSync } from "child_process"; +import axios from "axios"; + +/** + * Global teardown — removes all data created by Playwright during the test run. + * + * Strategy: + * 1. Log in as the seed admin account. + * 2. Fetch all users whose email ends in "@example.com" (the domain used by + * every test-data generator in this suite). + * 3. Delete their events first (activities use SET NULL on creator delete, + * so we must remove events before users to avoid orphaned rows). + * 4. Delete the test users. + * + * Seed users (@nsc.dev) are never touched. + */ + +const API = process.env.PLAYWRIGHT_API_URL || "http://localhost:3000/api"; +const ADMIN_CREDENTIALS = { + email: "admin@nsc.dev", + password: "admin@admin123", +}; +const TEST_EMAIL_DOMAIN = "@example.com"; + async function globalTeardown() { console.log("Starting global teardown..."); + // ── 1. Obtain admin token ────────────────────────────────────────────────── + let adminToken: string; + try { + const res = await axios.post(`${API}/auth/login`, ADMIN_CREDENTIALS); + adminToken = res.data?.token || res.data?.data?.token; + if (!adminToken) throw new Error("No token in login response"); + } catch (err: any) { + console.error( + " ✗ Could not obtain admin token for teardown:", + err.message, + ); + console.log(" Skipping teardown — seed admin account may not exist yet."); + return; + } + + const client = axios.create({ + baseURL: API, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + // ── 2. Fetch all users and filter to test accounts ───────────────────────── + let testUsers: any[] = []; + try { + const res = await client.get("/users"); + const allUsers: any[] = res.data?.data || res.data || []; + testUsers = allUsers.filter( + (u: any) => + typeof u.email === "string" && u.email.endsWith(TEST_EMAIL_DOMAIN), + ); + } catch (err: any) { + console.error(" ✗ Could not fetch users:", err.message); + return; + } + + if (testUsers.length === 0) { + console.log(" ~ No test users found — nothing to clean up."); + console.log("Global teardown completed\n"); + return; + } + + const testUserIds = new Set(testUsers.map((u: any) => u.id)); + console.log(` Found ${testUsers.length} test user(s) to remove`); + + // ── 3. Delete events created by test users ───────────────────────────────── + let deletedEvents = 0; try { - execSync("docker compose down --volumes --remove-orphans", { - stdio: "inherit", - }); - console.log("Global teardown completed successfully"); - } catch (error) { - console.error("Global teardown failed:", error); - throw error; + const eventsRes = await client.get(`/events?numberOfEventsToGet=500`); + // API may wrap in data.events, data.data, or return array directly + const raw = eventsRes.data; + const events: any[] = + raw?.data?.events ?? raw?.events ?? (Array.isArray(raw) ? raw : []); + + for (const event of events) { + const creatorId: string | undefined = + event.createdByUserId ?? event.creatorId ?? event.createdByUser?.id; + + if (creatorId && testUserIds.has(creatorId)) { + try { + await client.delete(`/events/remove/${event.id}`); + deletedEvents++; + } catch { + // Already deleted or not found — ignore + } + } + } + } catch (err: any) { + console.error(" ✗ Could not fetch/delete events:", err.message); + // Continue to user deletion even if event cleanup partial } + + // ── 4. Delete test users ─────────────────────────────────────────────────── + let deletedUsers = 0; + for (const user of testUsers) { + try { + await client.delete(`/users/remove/${user.id}`); + deletedUsers++; + } catch (err: any) { + console.error( + ` ✗ Could not delete user "${user.email}":`, + err.response?.data?.message ?? err.message, + ); + } + } + + console.log( + ` ✓ Removed ${deletedEvents} event(s) and ${deletedUsers}/${testUsers.length} user(s)`, + ); + + // (optional) -- Remove docker containers ----------------------------------------------- + // try { + // execSync("docker compose down --volumes --remove-orphans", { + // stdio: "inherit", + // }); + // console.log("Global teardown completed successfully"); + // } catch (error) { + // console.error("Global teardown failed:", error); + // throw error; + // } + // console.log( + // "\nRunning global teardown — cleaning up Playwright test data...", + // ); + + console.log("Global teardown completed\n"); } export default globalTeardown; diff --git a/package-lock.json b/package-lock.json index fa34263..700ef9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -882,7 +882,7 @@ "node_modules/@babel/core": { "version": "7.28.5", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1432,7 +1432,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1471,7 +1471,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1568,7 +1568,9 @@ }, "node_modules/@emotion/cache": { "version": "11.13.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", @@ -1594,7 +1596,9 @@ }, "node_modules/@emotion/react": { "version": "11.14.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1642,7 +1646,9 @@ }, "node_modules/@emotion/styled": { "version": "11.14.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2642,7 +2648,7 @@ "node_modules/@jest/transform": { "version": "29.7.0", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -2672,7 +2678,7 @@ "node_modules/@jest/types": { "version": "29.6.3", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -2793,7 +2799,9 @@ }, "node_modules/@mui/material": { "version": "5.18.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", + "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", @@ -3210,9 +3218,215 @@ "node": ">=6" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nestjs/cli/node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nestjs/cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/@nestjs/cli/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@nestjs/common": { "version": "10.4.20", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.20.tgz", + "integrity": "sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==", + "peer": true, "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", @@ -3264,7 +3478,7 @@ "node_modules/@nestjs/core": { "version": "10.4.20", "hasInstallScript": true, - "license": "MIT", + "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -3340,7 +3554,9 @@ }, "node_modules/@nestjs/platform-express": { "version": "10.4.20", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz", + "integrity": "sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==", + "peer": true, "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", @@ -3989,6 +4205,7 @@ "version": "1.58.2", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.58.2" }, @@ -5112,7 +5329,9 @@ }, "node_modules/@types/node": { "version": "22.14.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5182,7 +5401,9 @@ }, "node_modules/@types/react": { "version": "18.3.12", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5343,7 +5564,9 @@ }, "node_modules/@typescript-eslint/parser": { "version": "5.59.11", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.11.tgz", + "integrity": "sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.59.11", "@typescript-eslint/types": "5.59.11", @@ -5651,8 +5874,8 @@ }, "node_modules/accepts": { "version": "2.0.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -5663,7 +5886,9 @@ }, "node_modules/acorn": { "version": "8.15.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5674,7 +5899,6 @@ "node_modules/acorn-import-phases": { "version": "1.0.4", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" }, @@ -5713,7 +5937,7 @@ "node_modules/ajv": { "version": "8.12.0", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -6120,7 +6344,7 @@ "node_modules/babel-jest": { "version": "29.7.0", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -6418,7 +6642,7 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6659,11 +6883,15 @@ }, "node_modules/class-transformer": { "version": "0.5.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "peer": true }, "node_modules/class-validator": { "version": "0.14.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "peer": true, "dependencies": { "@types/validator": "^13.7.10", "libphonenumber-js": "^1.10.14", @@ -7039,8 +7267,8 @@ }, "node_modules/content-disposition": { "version": "1.0.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dependencies": { "safe-buffer": "5.2.1" }, @@ -7068,8 +7296,8 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "engines": { "node": ">=6.6.0" } @@ -7292,7 +7520,7 @@ "node_modules/date-fns": { "version": "2.30.0", "devOptional": true, - "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -7306,7 +7534,9 @@ }, "node_modules/dayjs": { "version": "1.11.19", - "license": "MIT" + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -7854,7 +8084,10 @@ }, "node_modules/eslint": { "version": "8.42.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", + "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -8014,7 +8247,9 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8655,8 +8890,8 @@ }, "node_modules/express": { "version": "5.1.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -8696,8 +8931,8 @@ }, "node_modules/express/node_modules/body-parser": { "version": "2.2.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -8715,8 +8950,8 @@ }, "node_modules/express/node_modules/iconv-lite": { "version": "0.6.3", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8726,16 +8961,16 @@ }, "node_modules/express/node_modules/media-typer": { "version": "1.1.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "engines": { "node": ">= 0.8" } }, "node_modules/express/node_modules/qs": { "version": "6.14.0", - "license": "BSD-3-Clause", - "peer": true, + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": { "side-channel": "^1.1.0" }, @@ -8748,8 +8983,8 @@ }, "node_modules/express/node_modules/raw-body": { "version": "3.0.1", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -8762,8 +8997,8 @@ }, "node_modules/express/node_modules/raw-body/node_modules/iconv-lite": { "version": "0.7.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8777,8 +9012,8 @@ }, "node_modules/express/node_modules/type-is": { "version": "2.0.1", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -8919,8 +9154,8 @@ }, "node_modules/finalhandler": { "version": "2.1.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -9081,8 +9316,8 @@ }, "node_modules/fresh": { "version": "2.0.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "engines": { "node": ">= 0.8" } @@ -10154,8 +10389,8 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "node_modules/is-regex": { "version": "1.2.1", @@ -10425,7 +10660,7 @@ "node_modules/jest": { "version": "29.7.0", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11557,7 +11792,7 @@ "node_modules/jsdom": { "version": "27.2.0", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -12125,8 +12360,8 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "engines": { "node": ">=18" }, @@ -12177,16 +12412,16 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "3.0.1", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dependencies": { "mime-db": "^1.54.0" }, @@ -12358,8 +12593,8 @@ }, "node_modules/negotiator": { "version": "1.0.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "engines": { "node": ">= 0.6" } @@ -12382,7 +12617,9 @@ }, "node_modules/next": { "version": "14.2.33", - "license": "MIT", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz", + "integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==", + "peer": true, "dependencies": { "@next/env": "14.2.33", "@swc/helpers": "0.5.5", @@ -13019,7 +13256,9 @@ }, "node_modules/passport": { "version": "0.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -13111,7 +13350,9 @@ }, "node_modules/pg": { "version": "8.16.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -13350,7 +13591,7 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13512,7 +13753,9 @@ }, "node_modules/preact": { "version": "10.27.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -13542,7 +13785,7 @@ "node_modules/prettier": { "version": "2.8.8", "dev": true, - "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -13724,7 +13967,9 @@ }, "node_modules/react": { "version": "18.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13780,7 +14025,9 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -14147,8 +14394,8 @@ }, "node_modules/router": { "version": "2.2.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -14162,8 +14409,8 @@ }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.3.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -14192,7 +14439,9 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -14310,7 +14559,7 @@ "node_modules/schema-utils/node_modules/ajv": { "version": "6.12.6", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14347,8 +14596,8 @@ }, "node_modules/send": { "version": "1.2.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -14376,8 +14625,8 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -14757,8 +15006,8 @@ }, "node_modules/statuses": { "version": "2.0.2", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "engines": { "node": ">= 0.8" } @@ -15499,7 +15748,9 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "peer": true, "engines": { "node": ">=12" }, @@ -15670,7 +15921,7 @@ "node_modules/ts-node": { "version": "10.9.1", "devOptional": true, - "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15898,7 +16149,9 @@ }, "node_modules/typeorm": { "version": "0.3.27", - "license": "MIT", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", + "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -15998,7 +16251,9 @@ }, "node_modules/typescript": { "version": "5.6.3", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16322,7 +16577,6 @@ "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -16330,7 +16584,6 @@ "node_modules/webpack/node_modules/mime-types": { "version": "2.1.35", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -16341,7 +16594,6 @@ "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16529,7 +16781,9 @@ }, "node_modules/winston": { "version": "3.18.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", diff --git a/package.json b/package.json index 335629f..2f31e7b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,10 @@ "test:e2e:codegen": "playwright codegen http://localhost", "test:all": "npm run test && npm run test:e2e", "docker": "docker compose down --rmi all --volumes --remove-orphans && docker compose build --no-cache && docker compose up -d", - "docker:watch": "npm run docker && docker compose logs -f nestjs" + "docker:watch": "npm run docker && docker compose logs -f nestjs", + "seed": "node scripts/events-seed.js", + "seed:users": "node scripts/users-seed.js", + "seed:all": "npm run seed:users && npm run seed" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/playwright.config.ts b/playwright.config.ts index a31632d..ace2320 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost"; @@ -42,7 +42,8 @@ export default defineConfig({ // Ensures a clean environment before every run — wipes volumes, rebuilds from scratch webServer: { - command: "docker compose down --volumes --remove-orphans && docker compose up -d && tail -f /dev/null", + command: + "docker compose down --volumes --remove-orphans && docker compose up -d && tail -f /dev/null", url: "http://localhost", reuseExistingServer: !process.env.CI, // CI always starts fresh; local dev may reuse timeout: 240000, diff --git a/scripts/events-seed.js b/scripts/events-seed.js new file mode 100644 index 0000000..a032ab1 --- /dev/null +++ b/scripts/events-seed.js @@ -0,0 +1,192 @@ +/** + * Seed script — populates the local database with sample events. + * + * Usage (from repo root): + * npm run seed + * + * Requires the NestJS backend to be running on port 3000. + * A seed creator account is created (or reused if it already exists). + */ + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const axios = require("axios"); + +const API = process.env.PLAYWRIGHT_API_URL || "http://localhost:3000/api"; + +const SEED_USER = { + email: "seed-creator@nsc.dev", + password: "Seed@Creator123", + firstName: "Seed", + lastName: "Creator", + pronouns: "they/them", + role: "creator", +}; + +// ─── Event templates ────────────────────────────────────────────────────────── + +const today = new Date(); +function d(daysOffset, hour = 9) { + const dt = new Date(today); + dt.setDate(dt.getDate() + daysOffset); + dt.setHours(hour, 0, 0, 0); + return dt.toISOString(); +} + +const EVENTS = [ + { + eventTitle: "Web Dev Workshop: Next.js 15 Deep Dive", + eventDescription: + "An in-depth hands-on workshop covering the latest features in Next.js 15, including server actions, partial pre-rendering, and the new caching model.", + startDate: d(3, 10), + endDate: d(3, 13), + eventLocation: "Building A, Room 201", + eventHost: "NSC Web Dev Club", + eventCapacity: "40", + eventContact: "webdev@nsc.dev", + tagNames: ["Tech", "Workshop", "Web"], + eventPrivacy: "Public", + eventSchedule: "10:00 AM – Intro\n11:00 AM – Hands-on Lab\n12:00 PM – Q&A", + eventSpeakers: ["Jane Smith"], + isHidden: false, + isArchived: false, + }, + { + eventTitle: "Cloud & AWS Study Group – S3 & IAM", + eventDescription: + "Study group focused on AWS fundamentals: S3 bucket policies, IAM roles, and access points. Bring your laptop.", + startDate: d(5, 14), + endDate: d(5, 16), + eventLocation: "Library, Study Room 4", + eventHost: "NSC Cloud Club", + eventCapacity: "20", + eventContact: "cloud@nsc.dev", + tagNames: ["AWS", "Cloud", "Study Group"], + eventPrivacy: "Public", + isHidden: false, + isArchived: false, + }, + { + eventTitle: "Spring Career Fair 2026", + eventDescription: + "Meet recruiters from 30+ tech companies. Bring printed résumés. Business casual dress required.", + startDate: d(10, 9), + endDate: d(10, 17), + eventLocation: "Main Campus Gymnasium", + eventHost: "NSC Career Services", + eventCapacity: "300", + eventContact: "careers@nsc.dev", + tagNames: ["Career", "Networking"], + eventPrivacy: "Public", + isHidden: false, + isArchived: false, + }, + { + eventTitle: "Hackathon: Build for Good", + eventDescription: + "24-hour hackathon with prizes. Build a project that addresses a real community need. Teams of 2–4.", + startDate: d(14, 8), + endDate: d(15, 8), + eventLocation: "Innovation Hub, Floor 3", + eventHost: "NSC ACM Chapter", + eventCapacity: "80", + eventContact: "acm@nsc.dev", + tagNames: ["Hackathon", "Tech", "Community"], + eventPrivacy: "Public", + eventSchedule: + "Day 1 08:00 – Kickoff\nDay 2 08:00 – Presentations\nDay 2 10:00 – Awards", + isHidden: false, + isArchived: false, + }, + { + eventTitle: "Intro to Machine Learning", + eventDescription: + "Beginner-friendly seminar covering supervised vs. unsupervised learning, common algorithms, and a live Python demo.", + startDate: d(7, 15), + endDate: d(7, 17), + eventLocation: "Science Hall, Room 110", + eventHost: "NSC AI Club", + eventCapacity: "60", + eventContact: "ai@nsc.dev", + tagNames: ["AI", "Python", "Workshop"], + eventPrivacy: "Public", + eventSpeakers: ["Dr. Alice Chen", "Bob Tanaka"], + isHidden: false, + isArchived: false, + }, + { + eventTitle: "Open Source Contribution Day", + eventDescription: + "Find a project, open a PR, get mentored. All skill levels welcome. Coffee and snacks provided.", + startDate: d(21, 10), + endDate: d(21, 14), + eventLocation: "CS Building, Open Lab", + eventHost: "NSC Open Source Club", + eventCapacity: "50", + eventContact: "opensource@nsc.dev", + tagNames: ["Open Source", "Git", "Community"], + eventPrivacy: "Public", + isHidden: false, + isArchived: false, + }, +]; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function getToken() { + // Try existing seed account first + try { + const res = await axios.post(`${API}/auth/login`, { + email: SEED_USER.email, + password: SEED_USER.password, + }); + const token = res.data?.token || res.data?.data?.token; + if (token) { + console.log("✓ Logged in as existing seed user"); + return token; + } + } catch { + // User doesn't exist yet — sign up below + } + + const res = await axios.post(`${API}/auth/signup`, SEED_USER); + const token = res.data?.token || res.data?.data?.token; + if (!token) throw new Error("Signup succeeded but no token returned"); + console.log("✓ Created seed user and logged in"); + return token; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function seed() { + console.log(`\nSeeding events against ${API}\n`); + + let token; + try { + token = await getToken(); + } catch (err) { + console.error("✗ Auth failed:", err.response?.data ?? err.message); + process.exit(1); + } + + const client = axios.create({ + baseURL: API, + headers: { Authorization: `Bearer ${token}` }, + }); + + let created = 0; + for (const event of EVENTS) { + try { + const res = await client.post("/events/new", event); + const id = res.data?.id || res.data?.data?.id; + console.log(` ✓ "${event.eventTitle}" (id: ${id})`); + created++; + } catch (err) { + const msg = err.response?.data?.message ?? err.message; + console.error(` ✗ "${event.eventTitle}" — ${JSON.stringify(msg)}`); + } + } + + console.log(`\nDone — ${created}/${EVENTS.length} events created.\n`); +} + +seed(); diff --git a/scripts/users-seed.js b/scripts/users-seed.js new file mode 100644 index 0000000..033fa92 --- /dev/null +++ b/scripts/users-seed.js @@ -0,0 +1,172 @@ +/** + * Seed script — populates the local database with sample user profiles. + * + * Usage (from repo root): + * npm run seed:users + * + * Requires the NestJS backend to be running on port 3000. + * New accounts are created via POST /auth/signup. If the email is already in + * use (HTTP 409) the existing account is updated via PATCH /users/update/:id + * using an admin token. + */ + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const axios = require("axios"); + +const API = process.env.PLAYWRIGHT_API_URL || "http://localhost:3000/api"; + +// The admin entry in SEED_USERS is used to obtain a token for update calls. +const ADMIN = { email: "admin@nsc.dev", password: "admin@admin123" }; + +// ─── User templates ────────────────────────────────────────────────────────── + +const SEED_USERS = [ + { + email: "seed-creator@nsc.dev", + password: "Seed@Creator123", + firstName: "Seed", + lastName: "Creator", + pronouns: "they/them", + role: "creator", + }, + { + email: "admin@nsc.dev", + password: "admin@admin123", + firstName: "Admin", + lastName: "TestAccount", + pronouns: "they/them", + role: "admin", + }, + { + email: "admin2@nsc.dev", + password: "Admin2@dev456", + firstName: "Admin2", + lastName: "TestAccount", + pronouns: "they/them", + role: "admin", + }, + { + email: "user@nsc.dev", + password: "User@Alex123", + firstName: "Alex", + lastName: "Rivera", + pronouns: "they/them", + role: "user", + }, + { + email: "user2@nsc.dev", + password: "User@Sam456", + firstName: "Sam", + lastName: "Park", + pronouns: "she/her", + role: "user", + }, + { + email: "user3@nsc.dev", + password: "User@Jordan789", + firstName: "Jordan", + lastName: "Lee", + pronouns: "they/them", + role: "user", + }, +]; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function getAdminToken() { + // Try logging in first + try { + const res = await axios.post(`${API}/auth/login`, ADMIN); + const token = res.data?.token || res.data?.data?.token; + if (token) return token; + } catch { + // Admin doesn't exist yet — will be created in the main loop + } + return null; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function seed() { + console.log(`\nSeeding users against ${API}\n`); + + // Ensure admin exists before processing the full list so we have a token + // for any update calls that follow. + for (const user of SEED_USERS) { + if (user.email === ADMIN.email) { + try { + await axios.post(`${API}/auth/signup`, user); + console.log(` ✓ Created "${user.email}" (role: ${user.role})`); + } catch { + // Signup failed — admin may already exist; getAdminToken() will login below. + console.log( + ` ~ Admin "${user.email}" already exists, skipping signup`, + ); + } + break; + } + } + + const adminToken = await getAdminToken(); + if (!adminToken) { + console.error(" ✗ Could not obtain admin token — aborting."); + process.exit(1); + } + + const client = axios.create({ + baseURL: API, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + let created = 0; + let updated = 0; + let failed = 0; + + for (const user of SEED_USERS) { + if (user.email === ADMIN.email) continue; // already handled above + + try { + await axios.post(`${API}/auth/signup`, user); + console.log(` ✓ Created "${user.email}" (role: ${user.role})`); + created++; + } catch (err) { + const status = err.response?.status; + const message = err.response?.data?.message ?? err.message ?? ""; + const alreadyExists = + status === 409 || message.toLowerCase().includes("already exists"); + + if (alreadyExists) { + // User exists — look up by email then patch + try { + const found = await client.get( + `/users/email/${encodeURIComponent(user.email)}`, + ); + const id = found.data?.id || found.data?.data?.id; + await client.patch(`/users/update/${id}`, { + firstName: user.firstName, + lastName: user.lastName, + pronouns: user.pronouns, + role: user.role, + }); + console.log(` ↻ Updated "${user.email}" (role: ${user.role})`); + updated++; + } catch (updateErr) { + const msg = updateErr.response?.data?.message ?? updateErr.message; + console.error( + ` ✗ "${user.email}" update failed — ${JSON.stringify(msg)}`, + ); + failed++; + } + } else { + console.error(` ✗ "${user.email}" — ${JSON.stringify(message)}`); + failed++; + } + } + } + + console.log( + `\nDone — ${created} created, ${updated} updated, ${failed} failed.\n`, + ); +} + +seed();