diff --git a/.env.example b/.env.example index b93de99..3d09451 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,30 @@ +# Nuxt SEO #NUXT_SITE_ENV="staging" NUXT_SITE_URL="https://hackathon.cslabs.be" NUXT_SITE_NAME="Le Hackathon du CSLabs" NUXT_SITE_DESCRIPTION="Le Hackathon du CSLabs : 48h pour imaginer, prototyper et présenter un projet tech en équipe." +NUXT_OG_IMAGE_SECRET="" + +# Cloudflare Turnstile NUXT_TURNSTILE_SITE_KEY="" NUXT_TURNSTILE_SECRET_KEY="" -NUXT_SMTP_HOST="" -NUXT_SMTP_PORT="8465" -NUXT_SMTP_USER="" -NUXT_SMTP_PASSWORD="" +# Email +NUXT_SMTP_HOST="localhost" +NUXT_SMTP_PORT="1025" +NUXT_SMTP_USER="test" +NUXT_SMTP_PASSWORD="test" NUXT_SMTP_REPLY_TO="event@cslabs.be" -NUXT_OG_IMAGE_SECRET="" - +# Supabase client SUPABASE_URL="https://supabase-hackathon.cslabs.be" -SUPABASE_KEY="" -SUPABASE_SECRET_KEY="" +SUPABASE_KEY="sb_publishable_XXX" +SUPABASE_SECRET_KEY="sb_secret_XXX" + +# Database DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres" -CLAMAV_HOST="" -CLAMAV_PORT="" +# ClamAV +CLAMAV_HOST="localhost" +CLAMAV_PORT="3310" diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index 395c39d..770e2fe 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -7,6 +7,14 @@ inputs: pnpm-version: description: 'The pnpm version to setup' required: false + working-directory: + description: 'The directory where pnpm install should run' + required: false + default: '.' + cache-dependency-path: + description: 'The pnpm lockfile path used for cache invalidation' + required: false + default: 'pnpm-lock.yaml' database-url: description: 'The database URL to use during dependency installation' required: false @@ -25,7 +33,6 @@ runs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - cache: true version: ${{ inputs.pnpm-version }} - name: Setup Node.js cache @@ -33,6 +40,7 @@ runs: with: node-version: ${{ inputs.node-version }} cache: pnpm + cache-dependency-path: ${{ inputs.cache-dependency-path }} - name: Show versions shell: bash @@ -42,6 +50,7 @@ runs: - name: Install dependencies shell: bash + working-directory: ${{ inputs.working-directory }} run: pnpm install --frozen-lockfile env: DATABASE_URL: ${{ inputs.database-url }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..d6f51a9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Build and deploy documentation + +on: + push: + branches: [ main, deploy ] + paths: [ "docs/**" ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +env: + NODE_VERSION: 22 + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./docs + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node + pnpm and install dependencies + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + cache-dependency-path: docs/pnpm-lock.yaml + working-directory: ./docs + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Build documentation + run: pnpm run build + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: docs/.vitepress/dist + + deploy: + name: Deploy + needs: build + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..c26b0d3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +.vitepress/dist +.vitepress/cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..b4dfca9 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,89 @@ +import { defineConfig } from "vitepress"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Hackathon Docs", + description: "Developer documentation for the Hackathon platform", + lang: "en-BE", + lastUpdated: true, + base: "/", + sitemap: { + hostname: "https://docs.hackathon.cslabs.be", + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + search: { + provider: "local", + }, + nav: [ + {text: "Onboarding", link: "/onboarding/"}, + {text: "Architecture", link: "/architecture/"}, + {text: "Frontend", link: "/frontend/"}, + {text: "Backend", link: "/backend/"}, + {text: "Operations", link: "/operations/"}, + {text: "ADRs", link: "/adrs/"}, + ], + sidebar: { + "/onboarding/": [ + { + text: "Onboarding", + items: [ + {text: "Overview", link: "/onboarding/"}, + {text: "Local Setup", link: "/onboarding/local-setup"}, + {text: "Environment", link: "/onboarding/environment"}, + ], + }, + ], + "/architecture/": [ + { + text: "Architecture", + items: [ + {text: "Overview", link: "/architecture/"}, + {text: "Auth & RBAC", link: "/architecture/auth-rbac"}, + {text: "Data Model", link: "/architecture/data-model"}, + ], + }, + ], + "/frontend/": [ + { + text: "Frontend", + items: [ + {text: "Overview", link: "/frontend/"}, + ], + }, + ], + "/backend/": [ + { + text: "Backend", + items: [ + {text: "Overview", link: "/backend/"}, + {text: "API", link: "/backend/api"}, + ], + }, + ], + "/operations/": [ + { + text: "Operations", + items: [ + {text: "Overview", link: "/operations/"}, + ], + }, + ], + "/adrs/": [ + { + text: "ADRs", + items: [ + {text: "Overview", link: "/adrs/"}, + {text: "Nuxt", link: "/adrs/nuxt"}, + {text: "Prisma", link: "/adrs/prisma"}, + {text: "Supabase", link: "/adrs/supabase"}, + ], + }, + ], + }, + socialLinks: [ + {icon: "github", link: "https://github.com/CSLabsNamur/hackathon-website/"}, + ], + outline: "deep", + }, +}); diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 0000000..b2a62d1 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, CSLabs + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/adrs/index.md b/docs/adrs/index.md new file mode 100644 index 0000000..4187d86 --- /dev/null +++ b/docs/adrs/index.md @@ -0,0 +1,35 @@ +--- +title: ADRs +description: Architecture Decision Records for the project. +--- + +# ADRs + +ADR means `Architecture Decision Record`. + +An ADR is a short document describing: + +- the context of a technical choice; +- the decision that was made; +- the main alternatives that were considered; +- the consequences of the decision. + +The goal is give the next maintainer enough context to understand why the code looks the way it does. + +## When to add one + +Add an ADR when a decision is likely to matter months later, for example: + +- changing the deployment model; +- choosing a major library (e.g. for test suites); +- changing the UI library; +- changing how authorization or data storage works, etc. + +Do not create ADRs for every small refactor. The goal is to capture decisions that are likely to be relevant in the +future, not to document every change. + +## Existing ADRs + +- [Nuxt](/adrs/nuxt) +- [Prisma](/adrs/prisma) +- [Supabase](/adrs/supabase) diff --git a/docs/adrs/nuxt.md b/docs/adrs/nuxt.md new file mode 100644 index 0000000..7822c46 --- /dev/null +++ b/docs/adrs/nuxt.md @@ -0,0 +1,47 @@ +--- +title: ADR - Nuxt +description: Why the project is built as a single Nuxt application. +--- + +# Nuxt + +## Context + +The project needs to serve a public website, a participant dashboard, an admin dashboard, and a server API. +It needs good SEO support, so SSR is a must. +Those surfaces share the same event data, the same authentication model, and the same deployment lifecycle. + +## Decision + +Use a single Nuxt application for the public frontend, the dashboards, and the Nitro API. + +## Why + +- Nuxt gives us a unified framework for both frontend and backend code, which fits the project well. +- The public pages and the dashboards can share components, styles, and data fetching logic. +- Authentication and permission logic only need to be implemented once. +- Deployment is simpler because the project remains a single deployable unit. +- Nuxt gives us SSR support out of the box. +- With Nuxt modules, we can easily integrate with SEO tools, analytics, and other services as needed. +- The opinionated structure of Nuxt helps keep the code organized, and future maintainers can quickly understand where + to find different types of code (e.g. pages, components, server routes, etc.). +- The project is not large enough to justify the overhead of maintaining separate repositories or a custom API server. + +## Alternatives considered + +### Separate frontend and backend repositories + +This was the case in the old website, and it caused a lot of overhead in terms of code duplication and deployment +complexity. NestJS is a great framework, very scalable and great to work with, but it's overkill for this platform. + +### Use a different full-stack framework (e.g. Next.js) + +Honnestly, it was solely a personnal choice. Next.js is a great framework, but I find Vue to be easier to work with, and +easier to onboard new developers on. I also just don't want to deal with useEffect. + +## Downsides + +- The repository structure is broader than a simple Nuxt marketing site, so documentation matters more. +- Developers need to understand both client and server conventions, even if they mostly work on one side. +- New maintainers might need to learn Vue and Nuxt a bit if they are not already familiar with it, though it should be + straightforward to pick up. diff --git a/docs/adrs/prisma.md b/docs/adrs/prisma.md new file mode 100644 index 0000000..52e4752 --- /dev/null +++ b/docs/adrs/prisma.md @@ -0,0 +1,51 @@ +--- +title: ADR - Prisma +description: Why Prisma is used as the ORM. +--- + +# Prisma + +## Context + +The project stores a fairly rich application model. We use complex queries, with a lot of relations to navigate, and we +want to keep the codebase readable and maintainable for future contributors. + +We also need a clear schema that is versioned in the repository, clean support for types, a way to handle migrations for +cleaner deployment, and a way to handle transactions across multiple queries when needed. + +## Decision + +Use Prisma as the object relational mapper (ORM) for the database. + +## Why + +- The schema is explicit and versioned in the repository. +- The generated client gives type-safe access to the database, shared accross the frontend and backend, with a clear API + for navigating relationships and handling transactions. +- The Prisma Migrate tool easily handles schema migrations. +- Hand-written SQL requires more discipline to respect security standards. +- Shared understanding improves when the schema is readable in one place. + +## Alternatives considered + +### Direct SQL everywhere + +This would offer maximum control, but the project would lose a lot of readability and consistency for everyday CRUD and +relationship-heavy logic. + +This would also require more work and discipline to ensure that queries are secure and that the schema is +well-documented and versioned. + +### Treat Supabase as the only data abstraction + +Supabase is part of the stack, and we could use its library to access the database, but we'd loose the benefits from a +dedicated application schema and typed access layer. + +## Consequences + +- More work to set up local environment, because the Prisma client needs to be generated and the migrations need to be + run before the application can start. +- We need to take Prisma into account when designing Docker images and deployment, to ensure that the client is + generated, and that the migrations are run correctly. +- Schema changes need client generation and migration discipline. +- The generated client and generated model files must not be edited manually. diff --git a/docs/adrs/supabase.md b/docs/adrs/supabase.md new file mode 100644 index 0000000..b2467dd --- /dev/null +++ b/docs/adrs/supabase.md @@ -0,0 +1,50 @@ +--- +title: ADR - Supabase +description: Why Supabase is used for auth and storage. +--- + +# Supabase + +## Context + +We obviously need a database, but we also need object storage. We also need to build an authentication system on top of +this. +Supabase provides all of these things in one package, and can be self-hosted. + +## Decision + +Use Supabase for authentication and object storage, on top of the main PostgreSQL database. + +## Why + +- If we use Supabase for the database and the object storage, we might as well use its authentication system as well. +- Supabase Auth gives the project a working sign-in flow without building auth from scratch. +- Supabase Storage is a practical fit for CV uploads, submission files, and event assets. +- The PostgreSQL database remains accessible to Prisma. + +## Alternatives considered + +### AppWrite + +AppWrite is a similar all-in-one backend solution, but it is not as mature as Supabase. The main downside is that its +database is only accessible through the AppWrite library, which would make it impossible to use Prisma. + +This would be okay if AppWrite had a migration system, since it can give us shared types as well, but it does not. +I didn't want to deal with maintaining a unique hand-written SQL file for the database schema, and I already had +experience with Prisma, so I ruled it out. + +AppWrite still seems like a good option for some simpler projects though, and the library rules out security issues that +can arise from hand-written SQL. + +### Build custom authentication in the application + +Possible, but difficult to justify for this project. +It would increase security-sensitive code and maintenance work. + +## Consequences + +- A valid Supabase account is not enough unless there is also a matching "database" user (in table User and Admin XOR + Participant). +- Self-hosting Supabase is not a trivial task, unfortunately. The IT Manager will need to be involved in the process. +- Local development is slightly heavier because auth, storage and database concerns all need to be configured correctly. +- We're dependent on Supabase for auth methods. For example, they don't have passkeys yet. diff --git a/docs/architecture/auth-rbac.md b/docs/architecture/auth-rbac.md new file mode 100644 index 0000000..f862e02 --- /dev/null +++ b/docs/architecture/auth-rbac.md @@ -0,0 +1,101 @@ +--- +title: Auth & RBAC +description: Authentication, application users, roles and permissions. +--- + +# Auth & RBAC + +Authentication and authorization are two separate concerns in the application: + +- Supabase proves who the user is, +- The application database decides what the user is allowed to do. + +That distinction is important because a valid Supabase account is not enough on its own. +The user also needs a valid application-side profile and matching roles. + +![AuthRBAC-seqdiag.png](/diagrams/AuthRBAC-seqdiag.png) + +## Identity model + +There are three important layers to keep in mind: + +- Supabase Auth user: the identity stored by Supabase, used for sign-in and session tokens. + - We store the Supabase user ID in `User.supabaseAuthId`, because we need it to link a JWT to an application user. +- `User`: the application-side user model, which includes profile information and role assignments. +- profile table: exactly one of `Admin` or `Participant`. + +The application enforces that a user has exactly one profile type. +A user cannot be both an admin and a participant, and they cannot be neither. + +## What happens on a protected API call + +For a protected route, the server does the following: + +1. Read the signed-in user from the request via its JWT. +2. Resolve the matching application `User` through `supabaseAuthId`. +3. Load the user's role assignments and permission keys. +4. Build (or get from cache) the CASL ability from those permissions. +5. Check the required permission before doing any sensitive work. + +This logic is centralized in the server utilities, which means new routes should reuse the existing helpers instead of +rolling their own auth checks. Read more about this in the [API documentation](/backend/api#authorization-patterns). + +## Roles and permissions + +Permissions are defined in the shared permission catalog. +Each permission maps to a CASL action/subject pair, for example: + +- `participants.read` -> `read` on `Participant` +- `teams.update.own` -> `updateOwn` on `Team` +- `broadcasts.send` -> `send` on `Broadcast` + +Roles are linked to permissions through `RolePermission`. +Users receive roles through `UserRoleAssignment`. + +Two system roles are seeded by default and **HAVE** to be in the database: + +- `participant` +- `super_admin` + +The application assumes that these roles keys always exist, and they are used in various places as special cases. +Please check `server/prisma/seed.ts` for the exact permissions assigned to them. + +Additional organizer roles can be created from the admin dashboard. + +## Client-side guards vs server-side enforcement + +The dashboards use route middleware to keep users away from pages they should not access. +This is useful for UX, but it is not the source of truth. Even if a user manages to load a page they should not see, the +API will still reject any unauthorized actions. + +That means: + +- missing middleware is a UX problem; +- missing server permission checks is a huge security problem. + +## Delegation rules + +Role management is strictly hierarchical: only a super-admin can assign or remove the `super_admin` role from an +organizer, and only an admin with a given permission can delegate that permission to another user. + +In practice, this prevents an organizer from creating a stronger role than their own role set. +The helper that enforces this is `assertCanDelegatePermissions` in `server/utils/ability.ts`. + +## Common debugging cases + +### I created a user on Supabase but they cannot log in + +This is because you didn't go through the API, and the user does not have a matching `User` row in the application +database, which is required to link them to a profile (Admin XOR Participant) and roles. +To fix this, create a `User` row with the correct `supabaseAuthId` and link it to a profile and roles. + +### I created a user and they can log in, but they get "Forbidden" when they try to do something + +This is because the user does not have the required permissions for that action. +Check the user's assigned roles and the permissions linked to those roles. +Check the permission catalog to see which permission is required for the action you're trying to perform. + +If nothing else works, check the logic for that endpoint or page to see if the required permission is correct, and if +the server is properly checking the user's permissions. +In the case of API routes, check if it's correctly making the difference between the Supabase user and the application +user. If the two are swapped, it won't work. diff --git a/docs/architecture/data-model.md b/docs/architecture/data-model.md new file mode 100644 index 0000000..ca3daa3 --- /dev/null +++ b/docs/architecture/data-model.md @@ -0,0 +1,104 @@ +--- +title: Data Model +description: The core entities in the application and how they relate to each other. +--- + +# Data Model + +The Prisma schema is the source of truth for the data model, that is, the tables, their fields, and their relationships. +This page is not a replacement for reading the schema, but rather a high-level guide to the most important entities and +how they fit together. +Its goal is to give high-level context on the main categories of tables and the key rules to keep in mind when working +with them. + +## Identity and access + +These tables define who exists in the application and what they can do: + +- `User` +- `Admin` +- `Participant` +- `Role` +- `Permission` +- `UserRoleAssignment` +- `RolePermission` + +The main rules to keep in mind are: + +- every authenticated person maps to one `User`; +- every `User` must have exactly one profile: `Admin` XOR `Participant`; +- a person's permissions are granted through roles assigned in `UserRoleAssignment`; +- permissions are defined in the shared permission catalog and linked to roles through `RolePermission`. + +![Identity-erdiag.png](/diagrams/Identity-erdiag.png) + +## Teams and submissions + +These tables describe the actual event participation: + +- `Team` +- `SubmissionRequest` +- `Submission` +- `SubmissionFile` +- `Room` +- `ScheduleItem` + +A submission request can be either individual or team-level, controlled by `SubmissionRequest.teamRequest`. + +In practice, this means: + +- some requests create one submission per participant; +- some requests create one submission per team, but they are still attached to a participant record for storage. + +![Participation-erdiag.png](/diagrams/Participation-erdiag.png) + +## Settings and event configuration + +These tables store the various settings that control how the event runs and what content is shown on the public website: + +- `WebsiteSettings` +- `EventSettings` +- `SocialLink` + +You can control things such as the event name, date, registration open/close dates, and the links shown in the website +header and footer. + +## Communication and operations + +Operational features have their own small models: + +- `EmailOutbox` for queued emails and retries; +- `Guest` for invited people and badge generation; +- `Sponsor` for partner management and badge generation. + +Sponsors are also linked to the public website "/partenaires" page. + +## Practical notes + +### Sensitive participant data + +Participant data is not all treated equally. +Some fields such as dietary preferences, specific needs, and newsletter choices are considered sensitive and are only +returned when the current ability includes the matching permission. + +### Supabase is not the main domain database + +Even though Supabase provides the PostgreSQL database, the project's actual domain model is described and accessed +through Prisma. +You should think in terms of Prisma models first. + +### Storage paths are references to Supabase Storage + +Fields such as uploaded CVs, submission files, and event assets are stored in Supabase Storage, and the database only +keeps references to their paths. +The actual file content lives in Supabase Storage, not in the repository and not on the application container's local +filesystem, and the application code is responsible for keeping those references in sync with the actual storage +content. + +If you were to add a new file field, you would need to: + +1. Create the corresponding path field in the Prisma schema, for example `cvPath` for a CV upload. +2. Handle the file upload in the API route, by using the Supabase Storage client to upload the file and get its path. +3. Store the path in the database through Prisma. +4. When serving the file, use the Supabase Storage client to generate a signed URL from the stored path, and return that + URL to the client. diff --git a/docs/architecture/index.md b/docs/architecture/index.md new file mode 100644 index 0000000..e0925ad --- /dev/null +++ b/docs/architecture/index.md @@ -0,0 +1,106 @@ +--- +title: Architecture Overview +description: The high-level view of the platform. +--- + +# Architecture Overview + +This project is a single Nuxt 4 application that serves several distinct surfaces: + +- the public website, on SSR, meant for marketing and event information; +- the participant dashboard, client-rendered and protected by authentication, meant for participants to manage their + profile, team and submissions; +- the admin dashboard, client-rendered and protected by authentication and permissions, meant for organizers to manage + the event. +- the server, which provides the API and handles background tasks. + +There is no separate backend repository. +The frontend and backend are developed together, shipped together, and share types and validation schemas from the same +codebase. That is fully intentional. + +If you want more information about the rationale behind this architecture, check out the [Nuxt ADR](/adrs/nuxt). + +## Big picture + +The application runs as one deployable unit with a few important external services around it: + +- a Nuxt 4 client application for the public site and both dashboards; +- an embedded Nitro server for API routes and scheduled tasks; +- Supabase; + - PostgreSQL database accessed through Prisma ORM + - Authentication + - Object Storage +- an SMTP server/relay used by Nodemailer; +- ClamAV for antivirus scanning on uploads. + +![Architecture-flowchart.png](/diagrams/Architecture-flowchart.png) + +The codebase is organized into four main directories, which follows the Nuxt 4 structure: + +- `app/` contains pages, layouts, components, composables, middleware and client-side utilities of the frontend. +- `server/` contains the API routes, server-side helpers, background tasks, mail handling, and Prisma code. +- `shared/` contains types, validation schemas, and permission definitions shared between the client and the server. +- `content/` contains the small amount of content managed through Nuxt Content for the public website. +- `docs/` contains this documentation. + +## What lives where + +If you need to find your way around quickly, this is the mental map to keep in mind: + +- Public routes such as `/`, `/infos`, `/partenaires` and `/historique` live under `app/pages/`. +- The participant dashboard lives under `app/pages/participant/`. +- The admin dashboard lives under `app/pages/admin/`. +- The API lives under `server/api/`. +- Database schema, migrations and seed data live under `server/prisma/`. +- Shared validation schemas live under `shared/schemas/`. + +## Key flows + +### Common request flow + +Most interactive flows follow this sequence: + +1. A page or component calls a composable such as `useParticipants`, `useRooms`, or `useCurrentAdmin`. +2. The composable uses the shared `$api` client from `app/plugins/api.ts`. +3. The API client automatically adds the bearer token from the Supabase session and the CSRF header. +4. The Nitro route checks the current user and permissions. +5. The route validates the input, usually through a schema from `shared/schemas/`. +6. The route reads or writes data through Prisma and sometimes through Supabase services as well. +7. The response comes back to the client, and the UI updates from there. + +### File-uploading flow + +File uploading is one of the most involved flows in the project. We will talk about it again in +the [API documentation](/backend/api#validation-patterns). + +A typical file upload flow looks like this: + +1. The public form submits multipart data. +2. The server receives it and parses the request with Formidable. +3. The uploaded file is checked for type concordance and scanned by ClamAV for viruses. +4. The non-file part of the form is validated with Valibot and a shared schema. +5. The file is renamed in some way to avoid name collisions and security issues. +6. The file is uploaded to Supabase Storage. + +### Email delivery + +The email pipeline is split in two parts: + +- HTML content is generated from MJML templates compiled into TypeScript render functions. +- Email jobs are stored in `EmailOutbox`, then processed by a Nitro scheduled task every 5 minutes. + +For detailed explanation, check out the [email documentation](/backend/#email). + +## Important architectural choices + +### Supabase for auth and storage, Prisma for the domain model + +Supabase handles authentication and object storage, and gives us a Postgres database to work with. + +Prisma is used for the actual application data model, as the ORM and migration tool. + +## Suggested next pages + +- [Auth & RBAC](/architecture/auth-rbac) +- [Data Model](/architecture/data-model) +- [Backend Overview](/backend/) diff --git a/docs/backend/api.md b/docs/backend/api.md new file mode 100644 index 0000000..df27e72 --- /dev/null +++ b/docs/backend/api.md @@ -0,0 +1,109 @@ +--- +title: API +description: Conventions and patterns used in the API. +--- + +# API + +This page describes how the API is organized and how new routes should fit into the existing structure. + +## File-based routing + +The API follows a RESTful structure. Routes live under `server/api/` and map directly to URLs, using Nitro's file-based +routing system. + +- `server/api/me.get.ts` -> `GET /api/me` +- `server/api/participants/index.get.ts` -> `GET /api/participants` +- `server/api/participants/me/index.put.ts` -> `PUT /api/participants/me` +- `server/api/roles/[id]/index.delete.ts` -> `DELETE /api/roles/:id` + +## API categories in this project + +The current API currently falls into these groups: + +- current user and auth context: `/api/me`, `/api/admins/me`, `/api/participants/me` +- participant management: `/api/participants/**` +- team management: `/api/teams/**` +- room and schedule management: `/api/rooms/**`, `/api/schedule/**` +- admin and role management: `/api/admins/**`, `/api/roles/**`, `/api/permissions` +- content and operations: `/api/sponsors/**`, `/api/guests/**`, `/api/broadcast` +- settings and exports: `/api/settings`, `/api/admin/settings/**`, `/api/admin/exports/**` +- submissions: `/api/submissions/**`, `/api/submissions/requests/**` + +## Common route shape + +A typical route handler looks like this: + +1. Check authentication and permissions with `requireSignedInUser(...)`, `requirePermission(...)`, or + `requireOrganizerAccess(...)`; +2. Get and validate input (e.g. query, body, params); +3. Read or write through Prisma; +4. perform one focused unit of work; +5. Return a typed JSON response or throw an `H3Error`. + +The route handler will probably call helpers from `server/utils/`, but the control flow should remain easy to read from +top to bottom. + +## Authorization patterns + +There are three common authorization entry points: + +- `requireSignedInUser(event)` when sign-in is enough; +- `requireOrganizerAccess(event)` for broader organizer-only access; + - This is a coarse check that the user is an organizer, but does not check specific permissions. +- `requirePermission(event, )` for permission-protected routes. + +Use the most explicit permission check possible. + +## Validation patterns + +For JSON routes, the preferred pattern is to validate with Valibot and a shared schema. + +For multipart routes, the flow is different (more details on why in the [overview](./index.md#validation) page): + +- parse with Formidable; +- validate file type and size. +- scan the file(s) with ClamAV; +- validate the non-file part with Valibot and a shared schema; + +Because of a bug in the [Nuxt Security](https://nuxt-security.vercel.app/) module, routes using Formidable must disable +the CSRF check. If enabled, this will cause Formidable parser to hang. +To disable this check, you need to set a route config in `nuxt.config.ts`, like this: + +```ts +export default defineNuxtConfig({ + // ... + routeRules: { + "/api/sponsors/index.post": { + csurf: false, + }, + }, +}); +``` + +> [!WARNING] +> So please, if you add a new multipart route, don't forget to disable the CSRF check for it. +> I've lost too much time searching why my code wasn't working before. + +## Response patterns + +The project does not try to wrap every response in a global envelope yet, but it's planned. +Many routes simply return the entity or collection directly. + +That said, there are a few practical conventions: + +- permission-sensitive routes should include or redact fields depending on the current ability; + - e.g. the participant list redacts email and other sensitive information for non-admins. +- routes should throw meaningful `statusMessage` values when possible, because the client toast layer displays them. + +> [!WARNING] +> So don't throw the entire error. **Be careful of leaking internal details.** +> Instead, throw an error using `createError` with a user-friendly message, and log the full error on the server for +> debugging. + +## Things to be careful about + +### Sensitive fields + +Don't assume that every admin-facing endpoint should automatically include all fields. +Some participant fields are permission-sensitive and the API already distinguishes them. diff --git a/docs/backend/index.md b/docs/backend/index.md new file mode 100644 index 0000000..d2d4cb6 --- /dev/null +++ b/docs/backend/index.md @@ -0,0 +1,128 @@ +--- +title: Backend Overview +description: How the server side of the project is structured. +--- + +# Backend Overview + +The backend is the `server/` directory of the Nuxt application. +It runs on Nuxt's embedded [Nitro](https://nitro.build/) server. + +## Main directory structure + +- `server/api/`: endpoints exposed under `/api/**` +- `server/utils/`: reusable server-side helpers +- `server/tasks/`: cron scheduled tasks +- `server/mail/`: MJML templates and their generated render functions +- `server/prisma/`: schema, migrations, seed, generated client +- `shared/schemas/`: [`Valibot`](https://valibot.dev/) validation schemas shared between client and server +- `shared/utils/`: shared permission and helper logic + +## Request lifecycle + +Most API routes follow the same pattern: + +1. Check authentication and permissions; +2. Get and validate input (e.g. query, body, params); +3. Read or write through Prisma; +4. Call external services if needed; +5. Return a typed JSON response or throw an `H3Error`. + +## Validation + +Validation is done with Valibot schemas from `shared/schemas/`, shared between the backend and frontend. +The frontend uses them for form validation, and the backend uses them to validate incoming requests. +This keeps the backend and frontend aligned on payload shape. + +Form validation is a bit more complex because of multipart parsing. Unfortunately, neither Nitro nor Valibot have +built-in support for multipart forms, so we have to resort to an external library for parsing the form data +([Formidable](https://github.com/node-formidable/formidable)), and then validate the non-file part with Valibot. +It's convoluted as heck, but it's the only I found to validate multipart forms. + +If you need to add a multipart form endpoint, check out `server/api/sponsors/index.post.ts` for an example of how to do +it. + +## Database access and management + +[Prisma](https://www.prisma.io/docs/orm) is the ORM for database access. +The schema, migrations, seed script and generated client all live under `server/prisma/`. +The configuration is at the project root, in `prisma.config.ts`. + +You will also find a `seed.ts` script, which you can run with `pnpm run db:seed` to populate the database with initial +data. + +### Client generation and usage + +Prisma works by generating a type-safe client based on the schema. In later versions, this client has to be generated +within the project sources, so we keep it under git-ignored `server/prisma/generated/`. +To generate the client, run `pnpm run db:generate`, which runs `prisma generate` under the hood. + +The generated client is re-exported using a server util, in `server/utils/prisma.ts` to make use of Nitro's autoimports. +If you need to extend the client, add your extension there. + +### Migrations and schema management + +Prisma has a built-in migration system, which we use to keep the database schema in sync with the Prisma schema. +Those have to be applied to the database with `pnpm run db:deploy`, which runs `prisma migrate deploy` under the hood. + +Whenever you make a change to the Prisma schema, you need to generate a migration with `pnpm run db:migrate`, which runs +`prisma migrate dev`. +This will ask you a name for the migration, generate the SQL, and apply it to the database. + +## External services + +### Supabase + +We use [Supabase](https://supabase.com/) for the database, authentication and object storage. + +The authentication layer is used for chceking if the request is properly authenticated, while the storage layer is used +for uploaded files and event assets. + +### Email + +We use [Nodemailer](https://nodemailer.com/) with SMTP for email delivery. + +In production, the SMTP server is provided by [SMTP2Go](https://www.smtp2go.com/), while in development, we use +[MailDev](https://github.com/maildev/maildev). + +#### Templates + +Our templating engine is [MJML](https://mjml.io/), which allows us to write responsive email templates without the usual +nightmares of email HTML. +The MJML files live under `server/mail/templates/`. + +Since we also need to render those templates with dynamic data, we use a placeholder library +called [Handlebars](https://handlebarsjs.com/) to inject variables into the templates. +That is why every template has a corresponding type definition `.d.ts` file alongside them, which defines the props that +the template expects. + +To assemble it all together, we have a generation script `scripts/generate-mail-templates.ts` that compiles the MJML +templates into render functions, with the proper typing, in `server/mail/generated/`. + +If you want to check out an example, check out `server/api/participants/index.post.ts` at the end, where we send a +confirmation email after participant registration. + +### Virus scanning + +We use [ClamAV](https://www.clamav.net/) for virus scanning on uploads. +The scanning logic is in `server/utils/clamav.ts`, and it's used in every endpoint that accepts file uploads. + +A ClamAV instance is included in the Docker Compose setup, and the backend connects to it over TCP. +All of this is configured through environment variables. + +If you want an example, check out `server/api/sponsors/index.post.ts`. + +## Email outbox + +To ensure reliable email delivery, we use an outbox pattern, where emails to be sent are stored in the database, and a +scheduled task processes the outbox and sends the emails, marking them as sent or failed accordingly. +This way, if the email service is down or there is a transient error, we don't lose any emails, and we can retry sending +them later. + +![MailOutbox-seqdiag.png](../public/diagrams/MailOutbox-seqdiag.png) +![MailOutbox-statediag.png](../public/diagrams/MailOutbox-statediag.png) + +## Cron jobs + +Nitro scheduled tasks are enabled in `nuxt.config.ts`. +At the moment, the main scheduled job processes the email outbox every 5 minutes. diff --git a/docs/frontend/index.md b/docs/frontend/index.md new file mode 100644 index 0000000..4afb9b8 --- /dev/null +++ b/docs/frontend/index.md @@ -0,0 +1,92 @@ +--- +title: Frontend Overview +description: How the client-side application is organized. +--- + +# Frontend Overview + +The frontend is the `app/` directory of the Nuxt application. +It includes the public website, the participant dashboard, and the admin dashboard. + +The public site is classic SSR (for SEO), while the dashboards are fully client-side applications. +You can look at the route rules in `nuxt.config.ts` to see how that works. + +## Main directory structure + +- `app/pages/`: routes +- `app/layouts/`: wrappers around pages, such as the public layout and dashboard layouts +- `app/components/`: reusable UI components +- `app/composables/`: data access and frontend logic +- `app/middleware/`: route guards +- `app/plugins/`: app-wide Nuxt plugins (e.g. the custom fetch client) +- `app/utils/`: shared client-side helpers +- `shared/schemas/`: [`Valibot`](https://valibot.dev/) validation schemas shared between client and server +- `shared/utils/`: shared permission and helper logic + +## Layouts + +There are three main layouts: + +- the default layout for the public site; +- the admin dashboard layout; +- the participant dashboard layout. + +The layouts do more than visual framing. +They also load the current user context, build the navigation and expose page action buttons (e.g. "Create new role" on +the roles page of the admin panel). + +## Data access pattern + +No page should not call `useFetch` or `$fetch` directly. In fact, nowhere should you call those directly, because we +have a custom wrapper around Nuxt's fetch API that adds auth headers, error handling, and CSRF protection. + +The usual pattern is: + +1. create or reuse a composable such as `useParticipants`, `useRoles`, `useRooms`, or `useSettings`; +2. let that composable call `useAPI(...)` if it's the main getter for the model, or the shared `$api` client for + model actions; +3. keep the page focused on page state and UI concerns. + +This gives us a consistent structure: + +- pages define the layout, route middleware and page meta; +- composables fetch and mutate; +- components render and emit events. + +## Access control on the frontend + +While the backend is the final authority on permissions, the frontend also has a concept of "ability" that it derives +from the current user context, so that it can hide or disable UI elements that the user shouldn't interact with. +This is implemented through the `useAbility` composable, which checks whether the current user can perform certain +actions on certain models. + +## Creating new pages - UI conventions + +If you want to create a new page, the biggest advice I could give is to check out the existing pages and follow their +patterns. +In general, check out if you: + +- set the right layout, middleware and permissions through `definePageMeta`; +- if it's a public page, set the SEO meta tags through `useSeoMeta` and other utils from Nuxt SEO; +- loaded the right data from the right composable; +- checked user permissions through `useAbility` and rendered the right UI elements accordingly. + +These ones are an absolute necessity. Beyond that, you can also check out the following UI conventions: + +- Use VueUse composables whenever you can, especially for common patterns like debouncing or getting the browser state; +- Use Nuxt UI components whenever you can, especially for common UI patterns like cards, tables, dropdowns, modals and + overlays; +- Followed the global UX direction of the website (e.g. for a page in the dashboard, "create" button on the top right, " + edit" and "delete" actions in the table row, etc.). +- Try and think of whether the code could be reused in other pages, and if so, whether it should be extracted to a + composable or a component (DRY). I'm not saying to abstract everything possible; I don't like abstracting a single-use + check or function, it often makes the code harder to read more than it helps. Use your own judgement here. + +## Good first places to look + +If you need practical examples, these pages are good references: + +- The registration page, for a simple public page with form submission and error handling; +- The admin home page, for permission-gated overview cards; +- The participants page in the admin panel, for a full dashboard CRUD view. + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e3dcd3e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Hackathon Docs" + text: "Developer documentation for the Hackathon platform" + actions: + - theme: brand + text: Start with Onboarding + link: /onboarding/ + - theme: alt + text: Explore the Architecture + link: /architecture/ + +features: + - title: Onboarding + details: Get the Hackathon platform running on your local machine in a few steps. + link: /onboarding/ + - title: Architecture + details: Understand the high-level structure of the application and how the different parts interact. + link: /architecture/ + - title: Frontend & Backend + details: Dive into the implementation details of the frontend and backend codebases. + link: /frontend/ + - title: ADRs + details: Review the main technical decisions made in the project and their rationale. + link: /adrs/ +--- + +## Getting started + +This documentation is meant for developers maintaining the Hackathon platform. +It is assumed that you already have the basics of web development. +This will also not replace the official documentation of the frameworks and libraries used in the project, but rather +complement it by providing a more focused view on how they are used in this specific context. + +The goal is to provide a comprehensive overview of the project, covering everything from the local setup to the +architectural decisions, how authentication is wired, where the API logic lives, which services the application depends +on, etc. + +If you are new to the project, start with these pages: + +- [Onboarding](/onboarding/) for the local setup and environment variables. +- [Architecture](/architecture/) for the high-level view of the application. +- [Frontend](/frontend/) and [Backend](/backend/) for the day-to-day implementation details. +- [ADRs](/adrs/) for the main technical choices already made in the project. diff --git a/docs/onboarding/environment.md b/docs/onboarding/environment.md new file mode 100644 index 0000000..7b953c6 --- /dev/null +++ b/docs/onboarding/environment.md @@ -0,0 +1,136 @@ +--- +title: Environment Setup +description: A guide to setting up the environment variables for the project. +--- + +# Environment Setup + +This guide will walk you through setting up the environment variables for the project. + +## Create a `.env` file + +During the [local setup guide](/onboarding/local-setup), you should have copied the `.env.example` file to `.env`. +This file contains all the environment variables needed for the project. + +## Update the environment variables + +The environment variables are grouped into sections based on their functionality. +We will go through each section and briefly explain what they are for and how to set them up. More detailed explanations +of each section or module will be provided in the respective documentation. + +### Nuxt SEO + +[Nuxt SEO](https://nuxtseo.com/) is a suite of modules that provides utilities and automatic configuration of +SEO-related features in Nuxt applications. +It gives us modules such as: + +- [Nuxt Robots](https://nuxtseo.com/docs/robots): Handles the generation of the `robots.txt` file, which is used to + control how search engines crawl and index the website; +- [Nuxt Sitemap](https://nuxtseo.com/docs/sitemap): Automatically generates a `sitemap.xml` file, which helps search + engines understand the structure of the website and find all the pages; +- [Nuxt OG Image](https://nuxtseo.com/docs/og-image): Automatically generates Open Graph images for social media + sharing; +- [Nuxt Schema-org](https://nuxtseo.com/docs/schema-org): Provides utilities for adding Schema.org structured data to + the website, which can improve search engine understanding and enhance search results; +- [Nuxt Link Checker](https://nuxtseo.com/docs/link-checker): Checks for broken links in the website and provides + reports; +- [Nuxt SEO Utils](https://nuxtseo.com/docs/seo-utils): Provides utilities for managing SEO-related data and + configurations; +- [Nuxt Site Config](https://nuxtseo.com/docs/site-config): Provides a centralized configuration for site-wide SEO + settings. Automatically installed with any module. + +Most of the modules are configured through the `nuxt.config.ts` file, but some of them require environment variables to +be set up, such as Site Config and OG Image. + +#### Site Config + +The Site Config module provides a centralized configuration for site-wide SEO settings. +It is automatically installed with any module, so you don't need to install it separately. + +The environment variables for the Site Config module are: + +- `NUXT_SITE_ENV`: The environement the site is running in. This is used to disable indexing for non-production + environments. It should be set to `production` in production and `development` in development. + Please note that this is NOT the same as `NODE_ENV`. +- `NUXT_SITE_URL`: The canonical URL of the site. This is used for SEO, OG Images and sitemaps. It should be set to the + URL of the site in production and `http://localhost:3000` in development. +- `NUXT_SITE_NAME`: The name of the site. This is used for meta tags and other SEO-related features. You should leave it + as is, i.e. "Le Hackathon du CSLabs". +- `NUXT_SITE_DESCRIPTION`: A description of the site. This is used for meta tags and other SEO-related features. You + should leave it as is, i.e. "Le Hackathon du CSLabs : 48h pour imaginer, prototyper et présenter un projet tech en + équipe !". + +#### OG Image + +The OG Image module automatically generates Open Graph images for social media sharing. + +The environment variables for the OG Image module are: + +- `NUXT_OG_IMAGE_SECRET`: A secret key used to prevent DoS attacks on the OG image generation endpoint. Use + `npx nuxt-og-image generate-secret` to generate a random secret key. + +### Cloudflare Turnstile + +[Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) is a CAPTCHA alternative that protects the website +from bots and spam. It is used in the registration form to prevent spam submissions. + +To configure it, you need to create a site key and a secret key in the Cloudflare dashboard. +Then, you need to set `NUXT_TURNSTILE_SITE_KEY` and `NUXT_TURNSTILE_SECRET_KEY` to the respective keys. + +The keys are already configured in Coolify. If you need to reconfigure them, you can find them in the +Cloudflare dashboard of the CSLabs account, managed by the IT manager. + +### Email + +The project uses SMTP to send emails, such as the confirmation email for registrations and broadcasts. + +If you followed the quick start guide, you already configured the SMTP server in Supabase, with or without Maildev. +Please note that this configuration is different; you need to configure the Nuxt application as well, which uses +Nodemailer to send emails directly. + +The environment variables for the email configuration are as you'd expect: + +- `NUXT_SMTP_HOST`: The hostname of the SMTP server. For Maildev, it should be `localhost`. +- `NUXT_SMTP_PORT`: The port of the SMTP server. For Maildev, it should be `1025`. +- `NUXT_SMTP_USER`: The username for the SMTP server. For Maildev, it should be `test`. +- `NUXT_SMTP_PASSWORD`: The password for the SMTP server. For Maildev, it should be `test`. +- `NUXT_SMTP_REPLY_TO`: The email address used in the "Reply-To" field of the emails sent by the application. It should + be set to an email address that you want to receive replies to, such as `event@cslabs.be`. + +### Supabase + +The project uses Supabase as the backend, which is a hosted PostgreSQL database with additional features such as +authentication and object storage. + +If you followed the quick start guide, you already configured Supabase and created a project. +In the configuration, you should have set a "Publishable key" and a "Secret key", which you need to set in the +environment variables as well. + +The environment variables for the Supabase configuration are: + +- `SUPABASE_URL`: The URL of the Supabase instance. For a local setup, it should be `http://localhost:8000`. +- `SUPABASE_KEY`: The publishable key of the Supabase project. It should start with `sb_publishable_`, for new + deployments. The Coolify deployment uses the older version, so it's a base64-encoded key. +- `SUPABASE_SECRET_KEY`: The secret key of the Supabase project. It should start with `sb_secret_`, for new deployments. + The Coolify deployment uses the older version, so it's a base64-encoded key. + +### Database + +The project uses a PostgreSQL database, which is provided by Supabase. +The connection is configured through `DATABASE_URL`, which should be set to the connection string of the PostgreSQL +database. + +To communicate with the database, Prisma is used, which uses this environment variable in `prisma.config.ts`. + +If you're running the local setup with Docker, don't forget to set your tenant ID in the `DATABASE_URL` as well, as +explained in the local setup guide. + +### ClamAV + +ClamAV is an open-source antivirus engine used to detect and prevent malware infections. +In the platform, it is used to scan any uploaded files for malware. + +The environment variables for the ClamAV configuration are: + +- `CLAMAV_HOST`: The hostname of the ClamAV server. For a local setup, it should be `localhost`. +- `CLAMAV_PORT`: The port of the ClamAV server. For a local setup, it should be `3310`. diff --git a/docs/onboarding/index.md b/docs/onboarding/index.md new file mode 100644 index 0000000..1904aca --- /dev/null +++ b/docs/onboarding/index.md @@ -0,0 +1,31 @@ +--- +title: Onboarding +--- + +# Onboarding + +If you're here, it either means you were assigned to maintaining this platform by your Event Admin, or you just want to +contribute to the project. + +Either way, welcome! This documentation will hopefully help you get started with everything. + +This section is the starting point. You will find guides on how to set up your development environment. +Other sections will cover the overall architecture, design choices, coding style, how the backend and the frontend are +structured, and more to come. + +To get started, check out the [Local Setup](/onboarding/local-setup) guide. +This will help you set up your development environment and get the project running on your machine. + +I heavily suggest you read through the [Architecture](/architecture/) section after that, as it will give you a good +overview of how the project is structured and how the different components interact with each other. + +A good reading order is the following: + +1. [Local Setup](/onboarding/local-setup) +2. [Environment Setup](/onboarding/environment) +3. [Architecture Overview](/architecture/) +4. [Frontend Overview](/frontend/) +5. [Backend Overview](/backend/) + +If you have any questions or need help, don't hesitate to reach out to me on Discord: `tinmar_`. I should still be +around on the CSLabs server, and happy to help you out. diff --git a/docs/onboarding/local-setup.md b/docs/onboarding/local-setup.md new file mode 100644 index 0000000..5a6ecc0 --- /dev/null +++ b/docs/onboarding/local-setup.md @@ -0,0 +1,289 @@ +--- +title: Local Setup +description: A guide to setting up your local development environment for the project. +--- + +# Local Setup + +This guide will walk you through setting up a local development environment for the project using Docker and Supabase. +By the end, you should be able to work on the project locally and test your changes before deploying them onto Coolify. + +## Prerequisites + +Before you start, make sure you have the following tools available: + +- Docker, for Supabase, Maildev and ClamAV +- Node.js 22 +- `corepack`, which should be included by default + +> [!NOTE] +> Fom Node.js 25 and up, `corepack` will not be included by default. + +You can then enable `pnpm` with: + +```sh +corepack enable +``` + +## Install Docker + +The instructions are available [here](https://docs.docker.com/get-started/get-docker/). + +## Setup Supabase in self-host mode + +The full documentation is available [here](https://supabase.com/docs/guides/self-hosting/docker), but here is a summary. + +Run the following commands to create a supabase self-host +project ([source](https://supabase.com/docs/guides/self-hosting/docker#installing-supabase)): + +```sh +# Get the code +git clone --depth 1 https://github.com/supabase/supabase +# Make your new supabase project directory +mkdir supabase-project +# Tree should look like this +# . +# ├── supabase +# └── supabase-project +# Copy the compose files over to your project +cp -rf supabase/docker/* supabase-project +# Copy the fake env vars +cp supabase/docker/.env.example supabase-project/.env +# Switch to your project directory +cd supabase-project +# Pull the latest images +docker compose pull +``` + +Generate the keys for the Supabase API using the following +command ([source](https://supabase.com/docs/guides/self-hosting/docker#quick-setup-experimental)): + +```sh +sh ./utils/generate-keys.sh +``` + +Add [maildev](https://github.com/maildev/maildev) to the `docker-compose.yml` in `supabase-project` to have a local SMTP +server for the authentication: + +```yml +name: supabase +services: + # ... + mail: + image: maildev/maildev:2.1.0 + environment: + - MAILDEV_WEB_USER=test + - MAILDEV_WEB_PASS=test + ports: + - "1080:1080" + - "1025:1025" +volumes: +# ... +``` + +Uncomment and change the following lines in the `docker-compose.yml` to enable the custom access token hook for +authentication: + +```yml +name: supabase +services: + # ... + auth: + # ... + environment: + # ... + GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" + GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook" + # ... + # ... + # ... +volumes: +# ... +``` + +Then, you can configure the `.env` in `supabase-project` to change the default passwords, keys and config. Here is +what is should look like at the end: + +```sh +# (...) + +# Postgres +POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password + +# (...) + +# Asymmetric key pair (ES256) and opaque API keys +# +# Documentation: +# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys +# +# To generate: +# sh ./utils/add-new-auth-keys.sh +# +# Opaque API key for client-side use (anon role). +SUPABASE_PUBLISHABLE_KEY=sb_publishable_ +# Opaque API key for server-side use (service_role). Never expose in client code. +SUPABASE_SECRET_KEY=sb_secret_ + +# (...) + +# Access to Dashboard +DASHBOARD_USERNAME=supabase +DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated + +# (...) + +# Access to Dashboard and REST API +SUPABASE_PUBLIC_URL=http://localhost:8000 + +# Full external URL of the Auth service, used to construct OAuth callbacks, +# SAML endpoints, and email links +API_EXTERNAL_URL=http://localhost:8000 + +# (...) + +# Using default user (postgres) +POSTGRES_HOST=db +POSTGRES_DB=postgres +POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password + +# Default configuration includes Supavisor exposing POSTGRES_PORT +# Postgres uses POSTGRES_PORT inside the container +# Documentation: +# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres-through-supavisor +POSTGRES_PORT=5432 + +# (...) + +# Unique Supavisor tenant identifier +# Documentation: +# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres +POOLER_TENANT_ID=your-tenant-id + +# (...) + +# Equivalent to "Site URL" and "Redirect URLs" platform configuration options +# Documentation: https://supabase.com/docs/guides/auth/redirect-urls +SITE_URL=http://localhost:3000 + +# (...) + +## Email auth (using maildev) +ENABLE_EMAIL_SIGNUP=true +ENABLE_EMAIL_AUTOCONFIRM=false +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_HOST=mail +SMTP_PORT=1025 +SMTP_USER=test +SMTP_PASS=test +SMTP_SENDER_NAME=fake_sender +ENABLE_ANONYMOUS_USERS=false + +# (...) +``` + +You can leave the default passwords/keys for a local setup, but you need to update the SMTP config at the end to match +the `maildev` config in `docker-compose.yml`. + +Finally, you can start supabase with the following +command ([source](https://supabase.com/docs/guides/self-hosting/docker#starting-and-stopping)): + +```sh +# Start the services (in detached mode) +docker compose up -d +``` + +## Setup the platform + +First, you need to clone the repo: + +```sh +git clone https://github.com/CSLabsNamur/hackathon-website.git +cd hackathon-website +``` + +Then, copy the example `.env` to the right place: + +```sh +cp .env.example .env +``` + +After that, update the `.env` to match the config of Supabase and Maildev. It should look like that: + +```sh +# (...) + +NUXT_SITE_URL="http://localhost:3000" + +# (...) + +NUXT_SMTP_HOST="localhost" +NUXT_SMTP_PORT="1025" +NUXT_SMTP_USER="test" +NUXT_SMTP_PASSWORD="test" +NUXT_SMTP_REPLY_TO="event@cslabs.be" + +SUPABASE_URL="http://localhost:8000" +SUPABASE_KEY="sb_publishable_" +SUPABASE_SECRET_KEY="sb_secret_" +DATABASE_URL="postgresql://postgres.your-tenant-id:your-super-secret-and-long-postgres-password@localhost:5432/postgres" + +# (...) +``` + +Just keep in mind the following 3 things: + +- The environment variable `SUPABASE_KEY` corresponds to the environment variable `SUPABASE_PUBLISHABLE_KEY` of + supabase. +- The environment variable `SUPABASE_SECRET_KEY` corresponds to the environment variable `SUPABASE_SECRET_KEY` of + supabase. +- The user in the environment variable `DATABASE_URL` is not just the `postgres` role. You also need to add the value of + your tenant ID from `POOLER_TENANT_ID` in the Supabase config: + `.` ([source](https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres-through-supavisor)) + because of the way Supavisor (the pooler) handles authentication and connection pooling. + +Next, you need to execute the `supabase_auth_hook.sql` and `supabase_rls.sql` scripts in the Supabase dashboard to +setup the access token hook and the RLS policies. You can do that by going to the SQL editor in the Supabase dashboard +and executing the content of those files. + +Finally, you can start the website: + +```sh +# In the separate `supabase-project` folder +docker compose up -d + +# In this repository +pnpm install +pnpm run db:migrate +pnpm run db:seed +pnpm run dev +``` + +`pnpm run db:seed` is important on a fresh database. +It creates the default settings rows, the system roles, and the initial admin profile stored in the application +database. By default, the initial admin profile has the email address `it@cslabs.be`. + +If you want to run the application container from this repository's own `docker-compose.yaml` instead, use: + +```sh +docker compose up --build +``` + +This compose file starts the app container and ClamAV. +It does not replace the separate Supabase self-hosted stack. + +> [!TIP] +> If you have a heap memory error while building the website, you can try increasing the Node.js heap size: +> +> ```sh +> export NODE_OPTIONS="--max-old-space-size=4096" +> ``` + +## Creating a user + +Participant accounts are created through the registration form. + +Organizer accounts are created by sending invites through the admin panel. Don't forget to assign them a fitting role. +Nobody should have permissions that they don't need. We follow the principle of least privilege, so if an organizer +doesn't need to manage the settings, don't give them access to the settings page. You can create custom roles with +custom permissions if needed as well. diff --git a/docs/operations/index.md b/docs/operations/index.md new file mode 100644 index 0000000..9d00344 --- /dev/null +++ b/docs/operations/index.md @@ -0,0 +1,56 @@ +--- +title: Operations Overview +description: An overview of the operational basics needed to keep the project running in development and CI. +--- + +# Operations Overview + +This page collects the operational basics needed to keep the project running in development and CI. +It is not yet a full deployment runbook, but it should be enough to avoid the most common mistakes. + +## CI + +The CI pipeline currently is relatively simple, because we don't have a test suite yet. It runs the following steps: + +1. install dependencies; +2. lint; +3. typecheck; +4. build. + +This way, we can at least catch build-time issues before they make it to production. + +## Deployment + +The repository includes: + +- a main `Dockerfile` for the application runtime; +- a `Dockerfile-prisma` image used for Prisma setup work; +- a `docker-compose.yaml` that wires them together. + +### Production + +To deploy the platform, we currently use Coolify on our server. It automatically triggers a build on push to the +`deploy` branch. +It uses the `docker-compose.yaml` to build the application and run it. + +The environment variables are set in Coolify itself, which is fully managed by the IT Manager. Either you check with +them first, or you ask for an account on Coolify to manage the deployment. + +The generation of the Prisma client is done by the `prisma_setup` service. +Migrations are applied as well, and the seeding script is executed. + +### Development preview + +To showcase the platform to the team, if necessary, we use the "Preview Deployments" feature of Coolify. It +automatically creates a deployment for a pull request, and it destroys it when the PR is closed. + +Environment variables are set in Coolify as well, in their own "Preview Deployments Environment Variables" section. + +## Things worth double-checking before shipping + +- the build completes successfully; +- the application starts without errors; +- **no dependency security issues are present** (check the output of `npm audit` and fix any critical issues before + shipping); +- every migration of the Prisma schema has been generated; +- environment variables are present and valid on Coolify. diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..021a957 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cslabs/hackathon-docs", + "description": "Developer documentation for the hackathon platform", + "private": true, + "type": "module", + "packageManager": "pnpm@9.15.9", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "author": "CSLabs", + "contributors": [ + { + "name": "Martin Jacob", + "email": "martin.jcb@proton.me" + } + ], + "license": "BSD-3-Clause", + "devDependencies": { + "vitepress": "^2.0.0-alpha.17", + "vue": "^3.5.32" + } +} \ No newline at end of file diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..501e89c --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,1391 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitepress: + specifier: ^2.0.0-alpha.17 + version: 2.0.0-alpha.17(postcss@8.5.9) + vue: + specifier: ^3.5.32 + version: 3.5.32 + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@4.6.2': + resolution: {integrity: sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==} + + '@docsearch/js@4.6.2': + resolution: {integrity: sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==} + + '@docsearch/sidepanel-js@4.6.2': + resolution: {integrity: sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.78': + resolution: {integrity: sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} + + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@6.0.6': + resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.32': + resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} + + '@vue/compiler-dom@3.5.32': + resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==} + + '@vue/compiler-sfc@3.5.32': + resolution: {integrity: sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==} + + '@vue/compiler-ssr@3.5.32': + resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==} + + '@vue/devtools-api@8.1.1': + resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} + + '@vue/devtools-kit@8.1.1': + resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} + + '@vue/devtools-shared@8.1.1': + resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} + + '@vue/reactivity@3.5.32': + resolution: {integrity: sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==} + + '@vue/runtime-core@3.5.32': + resolution: {integrity: sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==} + + '@vue/runtime-dom@3.5.32': + resolution: {integrity: sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==} + + '@vue/server-renderer@3.5.32': + resolution: {integrity: sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==} + peerDependencies: + vue: 3.5.32 + + '@vue/shared@3.5.32': + resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} + + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.2.1': + resolution: {integrity: sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 || ^8 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + focus-trap@8.0.1: + resolution: {integrity: sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress@2.0.0-alpha.17: + resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + oxc-minify: '*' + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + oxc-minify: + optional: true + postcss: + optional: true + + vue@3.5.32: + resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@docsearch/css@4.6.2': {} + + '@docsearch/js@4.6.2': {} + + '@docsearch/sidepanel-js@4.6.2': {} + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@iconify-json/simple-icons@1.2.78': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rolldown/pluginutils@1.0.0-rc.13': {} + + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/transformers@3.23.0': + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@6.0.6(vite@7.3.2)(vue@3.5.32)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.13 + vite: 7.3.2 + vue: 3.5.32 + + '@vue/compiler-core@3.5.32': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.32 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.32': + dependencies: + '@vue/compiler-core': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/compiler-sfc@3.5.32': + dependencies: + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.32 + '@vue/compiler-dom': 3.5.32 + '@vue/compiler-ssr': 3.5.32 + '@vue/shared': 3.5.32 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.9 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.32': + dependencies: + '@vue/compiler-dom': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/devtools-api@8.1.1': + dependencies: + '@vue/devtools-kit': 8.1.1 + + '@vue/devtools-kit@8.1.1': + dependencies: + '@vue/devtools-shared': 8.1.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.1.1': {} + + '@vue/reactivity@3.5.32': + dependencies: + '@vue/shared': 3.5.32 + + '@vue/runtime-core@3.5.32': + dependencies: + '@vue/reactivity': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/runtime-dom@3.5.32': + dependencies: + '@vue/reactivity': 3.5.32 + '@vue/runtime-core': 3.5.32 + '@vue/shared': 3.5.32 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.32(vue@3.5.32)': + dependencies: + '@vue/compiler-ssr': 3.5.32 + '@vue/shared': 3.5.32 + vue: 3.5.32 + + '@vue/shared@3.5.32': {} + + '@vueuse/core@14.2.1(vue@3.5.32)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.32) + vue: 3.5.32 + + '@vueuse/integrations@14.2.1(focus-trap@8.0.1)(vue@3.5.32)': + dependencies: + '@vueuse/core': 14.2.1(vue@3.5.32) + '@vueuse/shared': 14.2.1(vue@3.5.32) + vue: 3.5.32 + optionalDependencies: + focus-trap: 8.0.1 + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.32)': + dependencies: + vue: 3.5.32 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + csstype@3.2.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@7.0.1: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + focus-trap@8.0.1: + dependencies: + tabbable: 6.4.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + nanoid@3.3.11: {} + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + tabbable@6.4.0: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + trim-lines@3.0.1: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.2: + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@2.0.0-alpha.17(postcss@8.5.9): + dependencies: + '@docsearch/css': 4.6.2 + '@docsearch/js': 4.6.2 + '@docsearch/sidepanel-js': 4.6.2 + '@iconify-json/simple-icons': 1.2.78 + '@shikijs/core': 3.23.0 + '@shikijs/transformers': 3.23.0 + '@shikijs/types': 3.23.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.6(vite@7.3.2)(vue@3.5.32) + '@vue/devtools-api': 8.1.1 + '@vue/shared': 3.5.32 + '@vueuse/core': 14.2.1(vue@3.5.32) + '@vueuse/integrations': 14.2.1(focus-trap@8.0.1)(vue@3.5.32) + focus-trap: 8.0.1 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 3.23.0 + vite: 7.3.2 + vue: 3.5.32 + optionalDependencies: + postcss: 8.5.9 + transitivePeerDependencies: + - '@types/node' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - sass + - sass-embedded + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vue@3.5.32: + dependencies: + '@vue/compiler-dom': 3.5.32 + '@vue/compiler-sfc': 3.5.32 + '@vue/runtime-dom': 3.5.32 + '@vue/server-renderer': 3.5.32(vue@3.5.32) + '@vue/shared': 3.5.32 + + zwitch@2.0.4: {} diff --git a/docs/public/diagrams/Architecture-flowchart.mmd b/docs/public/diagrams/Architecture-flowchart.mmd new file mode 100644 index 0000000..dbfd3e4 --- /dev/null +++ b/docs/public/diagrams/Architecture-flowchart.mmd @@ -0,0 +1,25 @@ +flowchart LR + Browser["Browser"] + + subgraph Nuxt["Nuxt 4 application
single deployable unit"] + Public["Public pages
SSR"] + Dash["Dashboards
CSR"] + API["Nitro API routes"] + Tasks["Nitro scheduled tasks"] + end + + Browser --> Public + Browser --> Dash + + Public -->|"forms / page data"| API + Dash --> API + + API -->|"Database access"| Prisma["Prisma ORM"] + Prisma --> Supabase + API -->|"Authentication"| Supabase + API -->|"File storage"| Supabase + API -->|"Virus scanning"| AV["ClamAV"] + API --> SMTP["SMTP relay/server"] + + Tasks -->|"send queued emails"| SMTP + Tasks -->|"process email outbox"| Supabase \ No newline at end of file diff --git a/docs/public/diagrams/Architecture-flowchart.png b/docs/public/diagrams/Architecture-flowchart.png new file mode 100644 index 0000000..6ab2e2e Binary files /dev/null and b/docs/public/diagrams/Architecture-flowchart.png differ diff --git a/docs/public/diagrams/AuthRBAC-seqdiag.mmd b/docs/public/diagrams/AuthRBAC-seqdiag.mmd new file mode 100644 index 0000000..1e7acad --- /dev/null +++ b/docs/public/diagrams/AuthRBAC-seqdiag.mmd @@ -0,0 +1,21 @@ +sequenceDiagram + participant Client + participant API as API route + participant Supabase as Supabase Auth client + participant DB as Prisma / PostgreSQL + participant AuthH as Authorization Helper + + Client->>API: Request with Supabase session/JWT + API->>Supabase: Read authenticated user + Supabase-->>API: JWT plaintext payload + API->>DB: Find User by supabaseAuthId with JWT.sub + DB-->>API: User + profiles + roles + permissions + API->>AuthH: createAbilityForUser(dbUser) + AuthH-->>API: App ability + API->>AuthH: requirePermission("...") + AuthH-->>API: allowed or denied + alt Permission granted + API-->>Client: Continue route handler + else Permission denied + API-->>Client: 403 Forbidden + end diff --git a/docs/public/diagrams/AuthRBAC-seqdiag.png b/docs/public/diagrams/AuthRBAC-seqdiag.png new file mode 100644 index 0000000..0d19354 Binary files /dev/null and b/docs/public/diagrams/AuthRBAC-seqdiag.png differ diff --git a/docs/public/diagrams/Identity-erdiag.mmd b/docs/public/diagrams/Identity-erdiag.mmd new file mode 100644 index 0000000..700e7ae --- /dev/null +++ b/docs/public/diagrams/Identity-erdiag.mmd @@ -0,0 +1,43 @@ +erDiagram + USER[User] { + string id PK + string supabaseAuthId UK + string email UK + } + + ADMIN[Admin] { + string id PK + string userId FK + } + + PARTICIPANT[Participant] { + string id PK + string userId FK + } + + ROLE[Role] { + string id PK + string key UK + } + + PERMISSION[Permission] { + string id PK + string key UK + } + + USER_ROLE_ASSIGNMENT[UserRoleAssignment] { + string userId FK + string roleId FK + } + + ROLE_PERMISSION[RolePermission] { + string roleId FK + string permissionId FK + } + + USER ||--o| ADMIN : "has admin profile" + USER ||--o| PARTICIPANT : "has participant profile" + USER ||--o{ USER_ROLE_ASSIGNMENT : receives + ROLE ||--o{ USER_ROLE_ASSIGNMENT : assigned + ROLE ||--o{ ROLE_PERMISSION : grants + PERMISSION ||--o{ ROLE_PERMISSION : links diff --git a/docs/public/diagrams/Identity-erdiag.png b/docs/public/diagrams/Identity-erdiag.png new file mode 100644 index 0000000..b9929a3 Binary files /dev/null and b/docs/public/diagrams/Identity-erdiag.png differ diff --git a/docs/public/diagrams/MailOutbox-seqdiag.mmd b/docs/public/diagrams/MailOutbox-seqdiag.mmd new file mode 100644 index 0000000..d620a5d --- /dev/null +++ b/docs/public/diagrams/MailOutbox-seqdiag.mmd @@ -0,0 +1,26 @@ +sequenceDiagram + participant API as API route + participant Outbox as EmailOutbox table + participant Process as processEmailOutbox + participant Cron as emails:process task + participant Mailer as Nodemailer / SMTP + + API->>Outbox: enqueueEmail(...) + API->>API: request can complete + + Note over Cron,Outbox: Nitro runs emails:process every 5 minutes + Cron->>Process: processEmailOutbox() + Process->>Outbox: load PENDING / FAILED jobs + Process->>Outbox: claim one job as PROCESSING + Process->>Mailer: send mail + + alt send succeeds + Mailer-->>Process: accepted + Process->>Outbox: mark SENT + else send fails, retries remain + Mailer-->>Process: error + Process->>Outbox: mark PENDING and reschedule + else retries exhausted + Mailer-->>Process: error + Process->>Outbox: mark FAILED + end diff --git a/docs/public/diagrams/MailOutbox-seqdiag.png b/docs/public/diagrams/MailOutbox-seqdiag.png new file mode 100644 index 0000000..3d51a3e Binary files /dev/null and b/docs/public/diagrams/MailOutbox-seqdiag.png differ diff --git a/docs/public/diagrams/MailOutbox-statediag.mmd b/docs/public/diagrams/MailOutbox-statediag.mmd new file mode 100644 index 0000000..0f45616 --- /dev/null +++ b/docs/public/diagrams/MailOutbox-statediag.mmd @@ -0,0 +1,7 @@ +stateDiagram-v2 + [*] --> PENDING: enqueueEmail + PENDING --> PROCESSING: claimed by processEmailOutbox + PROCESSING --> SENT: SMTP send succeeds + PROCESSING --> PENDING: send fails, retry scheduled + PROCESSING --> FAILED: max attempts reached + FAILED --> PROCESSING: retried manually / selected by id diff --git a/docs/public/diagrams/MailOutbox-statediag.png b/docs/public/diagrams/MailOutbox-statediag.png new file mode 100644 index 0000000..450d4e0 Binary files /dev/null and b/docs/public/diagrams/MailOutbox-statediag.png differ diff --git a/docs/public/diagrams/Participation-erdiag.mmd b/docs/public/diagrams/Participation-erdiag.mmd new file mode 100644 index 0000000..cf42e93 --- /dev/null +++ b/docs/public/diagrams/Participation-erdiag.mmd @@ -0,0 +1,41 @@ +erDiagram + PARTICIPANT { + string id PK + string userId FK + string teamId FK + } + + TEAM { + string id PK + string roomId FK + string token UK + } + + ROOM { + string id PK + int sequence UK + } + + SUBMISSION_REQUEST { + string id PK + boolean teamRequest + string type + } + + SUBMISSION { + string id PK + string requestId FK + string participantId FK + } + + SUBMISSION_FILE { + string id PK + string submissionId FK + string path + } + + ROOM o|--o{ TEAM : hosts + TEAM o|--o{ PARTICIPANT : "is part of" + PARTICIPANT ||--o{ SUBMISSION : creates + SUBMISSION_REQUEST ||--o{ SUBMISSION : asks_for + SUBMISSION ||--o{ SUBMISSION_FILE : contains diff --git a/docs/public/diagrams/Participation-erdiag.png b/docs/public/diagrams/Participation-erdiag.png new file mode 100644 index 0000000..be06f74 Binary files /dev/null and b/docs/public/diagrams/Participation-erdiag.png differ diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/nuxt.config.ts b/nuxt.config.ts index b256f9e..a1f5a2c 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,7 +2,7 @@ const supabaseHostname = new URL(process.env.SUPABASE_URL || "http://localhost") // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ - compatibilityDate: "2025-07-15", + compatibilityDate: "2026-04-16", devtools: {enabled: true}, experimental: { checkOutdatedBuildInterval: 1000 * 60, // 1 minute diff --git a/package.json b/package.json index cb60baf..e545f01 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@cslabs/hackathon-website", "description": "CSLabs' Hackathon Website", - "version": "1.6.0", + "version": "1.7.0", "type": "module", "license": "BSD-3-Clause", "author": "CSLabs", @@ -14,7 +14,7 @@ "private": true, "repository": { "type": "git", - "url": "git+https://github.com/CSLabsNamur/hackathon-front-nuxt.git" + "url": "git+https://github.com/CSLabsNamur/hackathon-website.git" }, "scripts": { "postinstall": "nuxt prepare && pnpm run db:generate", @@ -34,7 +34,10 @@ "db:deploy": "prisma migrate deploy", "db:push": "prisma db push", "db:seed": "prisma db seed", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "docs:dev": "pnpm --dir docs run dev", + "docs:build": "pnpm --dir docs run build", + "docs:preview": "pnpm --dir docs run preview" }, "packageManager": "pnpm@9.15.9", "dependencies": {