Compare commits
30 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c83d8ee6da | |||
| 3200c42b29 | |||
| 3198819f75 | |||
| a64989c997 | |||
| 33552aa648 | |||
| caa0d580a0 | |||
| 46d9ff26dd | |||
| 8b2d2649ac | |||
| 7cdc561dcc | |||
| c4e3a31b69 | |||
| dad96978e0 | |||
| 560b40503a | |||
| 19ce2224b8 | |||
| 0e6fad65e2 | |||
| d5880d5096 | |||
| 93922e4cad | |||
| 21a2ffbd44 | |||
| 6b0e91798f | |||
| da037e9a79 | |||
| d562e41528 | |||
| b7d58d308a | |||
| 99d994544d | |||
| 270cee44ff | |||
| 061126c51e | |||
| ff52be8adf | |||
| a4c6b04b37 | |||
| 53261dec0c | |||
| 25a3eae628 | |||
| aba8801d21 | |||
| abd7e3a97f |
92 changed files with 2388 additions and 1693 deletions
10
.env.example
10
.env.example
|
|
@ -7,13 +7,3 @@ DB_NAME=mailserver
|
||||||
|
|
||||||
# Security - JWT secret must be at least 32 characters
|
# Security - JWT secret must be at least 32 characters
|
||||||
JWT_SECRET=your-secret-key-at-least-32-chars
|
JWT_SECRET=your-secret-key-at-least-32-chars
|
||||||
|
|
||||||
# SMTP settings for password reset emails
|
|
||||||
SMTP_HOST=localhost
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=noreply@yourdomain.com
|
|
||||||
|
|
||||||
# Application URL
|
|
||||||
BASE_URL=http://localhost:8080
|
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,7 +4,7 @@ build/
|
||||||
backend/cmd/server/embed/
|
backend/cmd/server/embed/
|
||||||
backend/tmp/
|
backend/tmp/
|
||||||
.env
|
.env
|
||||||
backend/imc-vibe
|
backend/imc
|
||||||
node_modules/
|
node_modules/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/build/
|
frontend/build/
|
||||||
|
|
|
||||||
27
Makefile
27
Makefile
|
|
@ -1,14 +1,22 @@
|
||||||
.PHONY: all dev dev-frontend dev-backend build build-frontend build-backend clean test lint
|
.PHONY: all dev dev-frontend dev-backend build build-frontend build-backend clean test lint sqlc
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
APP_NAME := imc-vibe
|
APP_NAME := imc
|
||||||
FRONTEND_DIR := frontend
|
FRONTEND_DIR := frontend
|
||||||
BACKEND_DIR := backend
|
BACKEND_DIR := backend
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
|
GOBIN := $(shell go env GOPATH)/bin
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
# Generate Go code from SQL queries using sqlc
|
||||||
|
# Requires: go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
|
sqlc:
|
||||||
|
@echo "Generating Go code from SQL queries..."
|
||||||
|
cd $(BACKEND_DIR) && $(GOBIN)/sqlc generate
|
||||||
|
@echo "SQL code generated successfully"
|
||||||
|
|
||||||
# Build everything (frontend + backend + embed)
|
# Build everything (frontend + backend + embed)
|
||||||
build: build-frontend build-backend
|
build: build-frontend build-backend
|
||||||
|
|
||||||
|
|
@ -20,7 +28,7 @@ build-frontend:
|
||||||
@echo "Frontend built successfully"
|
@echo "Frontend built successfully"
|
||||||
|
|
||||||
# Build backend only (copies frontend build into embed directory)
|
# Build backend only (copies frontend build into embed directory)
|
||||||
build-backend: build-frontend
|
build-backend: sqlc build-frontend
|
||||||
@echo "Copying frontend to embed directory..."
|
@echo "Copying frontend to embed directory..."
|
||||||
rm -rf $(BACKEND_DIR)/cmd/server/embed
|
rm -rf $(BACKEND_DIR)/cmd/server/embed
|
||||||
cp -r $(FRONTEND_DIR)/build $(BACKEND_DIR)/cmd/server/embed
|
cp -r $(FRONTEND_DIR)/build $(BACKEND_DIR)/cmd/server/embed
|
||||||
|
|
@ -30,8 +38,11 @@ build-backend: build-frontend
|
||||||
@echo "Backend built successfully"
|
@echo "Backend built successfully"
|
||||||
|
|
||||||
# Development targets
|
# Development targets
|
||||||
dev: dev-frontend dev-backend
|
dev:
|
||||||
@echo "Development mode running..."
|
@echo "Starting backend dev server (hot-reload enabled)..."
|
||||||
|
cd $(BACKEND_DIR) && USE_EMBEDDED=false $(GOBIN)/air &
|
||||||
|
@echo "Starting frontend dev server..."
|
||||||
|
cd $(FRONTEND_DIR) && bun run dev
|
||||||
|
|
||||||
dev-frontend:
|
dev-frontend:
|
||||||
@echo "Starting frontend dev server..."
|
@echo "Starting frontend dev server..."
|
||||||
|
|
@ -39,7 +50,7 @@ dev-frontend:
|
||||||
|
|
||||||
dev-backend:
|
dev-backend:
|
||||||
@echo "Starting backend dev server (hot-reload enabled)..."
|
@echo "Starting backend dev server (hot-reload enabled)..."
|
||||||
cd $(BACKEND_DIR) && USE_EMBEDDED=false PATH=$(PATH):$$HOME/go/bin air
|
cd $(BACKEND_DIR) && USE_EMBEDDED=false $(GOBIN)/air
|
||||||
|
|
||||||
# Clean build artifacts
|
# Clean build artifacts
|
||||||
clean:
|
clean:
|
||||||
|
|
@ -65,10 +76,12 @@ help:
|
||||||
@echo " all - Build frontend and backend (default)"
|
@echo " all - Build frontend and backend (default)"
|
||||||
@echo " build - Build frontend and backend"
|
@echo " build - Build frontend and backend"
|
||||||
@echo " build-frontend - Build frontend only"
|
@echo " build-frontend - Build frontend only"
|
||||||
@echo " build-backend - Build backend only"
|
@echo " build-backend - Build backend only (runs sqlc first)"
|
||||||
|
@echo " sqlc - Generate Go code from SQL queries"
|
||||||
@echo " dev - Start both frontend and backend with hot-reload"
|
@echo " dev - Start both frontend and backend with hot-reload"
|
||||||
@echo " dev-frontend - Start frontend dev server"
|
@echo " dev-frontend - Start frontend dev server"
|
||||||
@echo " dev-backend - Start backend dev server (requires: go install github.com/air-verse/air@latest)"
|
@echo " dev-backend - Start backend dev server (requires: go install github.com/air-verse/air@latest)"
|
||||||
|
@echo " sqlc - Generate Go code from SQL queries (requires: go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest)"
|
||||||
@echo " clean - Remove build artifacts"
|
@echo " clean - Remove build artifacts"
|
||||||
@echo " test - Run backend tests"
|
@echo " test - Run backend tests"
|
||||||
@echo " lint - Run Go vet"
|
@echo " lint - Run Go vet"
|
||||||
|
|
|
||||||
136
README.md
136
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
# IMC Vibe
|
# IMC
|
||||||
|
|
||||||
A self-sufficient web application to manage an ISPmail (Postfix, Dovecot, Rspamd) mail server.
|
A self-sufficient web application to manage an ISPmail (Postfix, Dovecot, Rspamd) mail server.
|
||||||
|
|
||||||
|
|
@ -9,11 +9,11 @@ A self-sufficient web application to manage an ISPmail (Postfix, Dovecot, Rspamd
|
||||||
- **Alias Management** - Create and manage email aliases per domain
|
- **Alias Management** - Create and manage email aliases per domain
|
||||||
- **Mail Queue** - View, requeue, and delete queued emails
|
- **Mail Queue** - View, requeue, and delete queued emails
|
||||||
- **Mail Logs** - View postfix logs with filtering
|
- **Mail Logs** - View postfix logs with filtering
|
||||||
- **Password Reset** - SMTP-based password reset functionality
|
- **Password Management** - Change password for admin users
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Backend**: Go with net/http (no framework), GORM for database
|
- **Backend**: Go with Gin framework, sqlc for type-safe SQL
|
||||||
- **Frontend**: SvelteKit
|
- **Frontend**: SvelteKit
|
||||||
- **Database**: MariaDB/MySQL (shared with mail server ISPmail schema)
|
- **Database**: MariaDB/MySQL (shared with mail server ISPmail schema)
|
||||||
- **Auth**: JWT-based authentication
|
- **Auth**: JWT-based authentication
|
||||||
|
|
@ -25,6 +25,38 @@ A self-sufficient web application to manage an ISPmail (Postfix, Dovecot, Rspamd
|
||||||
- Permissions controlled via `imc_users2domains` table
|
- Permissions controlled via `imc_users2domains` table
|
||||||
- Admin users have access to all domains; non-admin users only to assigned domains
|
- Admin users have access to all domains; non-admin users only to assigned domains
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
imc-vibe/
|
||||||
|
├── backend/ # Go backend
|
||||||
|
│ ├── cmd/server/ # Application entry point
|
||||||
|
│ │ ├── main.go # Main function, CLI flags, HTTP server setup
|
||||||
|
│ │ ├── frontend.go # Embedding the SvelteKit build
|
||||||
|
│ │ └── embed/ # Embedded frontend files (generated)
|
||||||
|
│ └── internal/ # Internal packages
|
||||||
|
│ ├── api/ # HTTP API routing and handlers
|
||||||
|
│ │ ├── router.go # Gin route definitions
|
||||||
|
│ │ └── handlers/ # HTTP handlers
|
||||||
|
│ ├── auth/ # JWT authentication
|
||||||
|
│ ├── config/ # Configuration loading
|
||||||
|
│ ├── db/ # Database access layer
|
||||||
|
│ │ ├── queries/ # SQL query files (sqlc source)
|
||||||
|
│ │ └── sqlc/ # Generated type-safe Go code
|
||||||
|
│ └── mail/ # Mail server integration (postqueue, logs, quota)
|
||||||
|
├── frontend/ # SvelteKit frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── app.css # Global CSS (Tailwind + daisyUI)
|
||||||
|
│ │ ├── app.html # HTML template
|
||||||
|
│ │ ├── lib/ # Shared utilities
|
||||||
|
│ │ └── routes/ # SvelteKit routes (pages)
|
||||||
|
│ ├── vite.config.ts # Vite configuration
|
||||||
|
│ └── package.json # Frontend dependencies
|
||||||
|
├── build/ # Built binary output (gitignored)
|
||||||
|
├── Makefile # Build automation
|
||||||
|
└── .env # Environment configuration (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Build
|
### 1. Build
|
||||||
|
|
@ -33,18 +65,18 @@ A self-sufficient web application to manage an ISPmail (Postfix, Dovecot, Rspamd
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Setup Admin User
|
### 2. Create Admin User
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/imc-vibe --setup
|
./build/imc --reset-admin-password
|
||||||
# Or with custom credentials:
|
# This creates an admin user or resets the password
|
||||||
./build/imc-vibe --setup --admin-user=admin --admin-password=yourpassword
|
# Output: Username: admin, Password: (randomly generated)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Run
|
### 3. Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/imc-vibe --bind=0.0.0.0 --port=8080
|
./build/imc --bind=0.0.0.0 --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Access
|
### 4. Access
|
||||||
|
|
@ -64,31 +96,21 @@ Open `http://your-server:8080` and login with the admin credentials.
|
||||||
| `DB_NAME` | Database name | `mailserver` |
|
| `DB_NAME` | Database name | `mailserver` |
|
||||||
| `BIND` | IP to bind to | `0.0.0.0` |
|
| `BIND` | IP to bind to | `0.0.0.0` |
|
||||||
| `PORT` | Port to listen on | `8080` |
|
| `PORT` | Port to listen on | `8080` |
|
||||||
| `JWT_SECRET` | JWT signing secret | (required) |
|
| `JWT_SECRET` | JWT signing secret (min 32 chars) | (required) |
|
||||||
| `SMTP_HOST` | SMTP server for password reset | `localhost` |
|
|
||||||
| `SMTP_PORT` | SMTP port | `587` |
|
|
||||||
| `SMTP_USER` | SMTP username | |
|
|
||||||
| `SMTP_PASSWORD` | SMTP password | |
|
|
||||||
| `SMTP_FROM` | From address for emails | `noreply@localhost` |
|
|
||||||
| `BASE_URL` | Base URL for password reset links | `http://localhost:8080` |
|
|
||||||
|
|
||||||
### CLI Flags
|
### CLI Flags
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./imc-vibe --help
|
./imc --help
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
-admin-password string
|
|
||||||
Admin password for setup (required with --setup)
|
|
||||||
-admin-user string
|
|
||||||
Admin username for setup (default "admin")
|
|
||||||
-bind string
|
-bind string
|
||||||
IP address to bind to (default: 0.0.0.0)
|
IP address to bind to (default: 0.0.0.0)
|
||||||
-port string
|
-port string
|
||||||
Port to listen on (default: 8080)
|
Port to listen on (default: 8080)
|
||||||
-setup
|
-reset-admin-password
|
||||||
Create admin user and exit
|
Reset admin password to a random value and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Tables
|
## Database Tables
|
||||||
|
|
@ -98,17 +120,36 @@ The app creates these tables automatically:
|
||||||
- `imc_users` - App admin users
|
- `imc_users` - App admin users
|
||||||
- `imc_login_attempts` - Brute force protection
|
- `imc_login_attempts` - Brute force protection
|
||||||
- `imc_users2domains` - User-domain permissions
|
- `imc_users2domains` - User-domain permissions
|
||||||
- `imc_password_reset_tokens` - Password reset tokens
|
|
||||||
|
|
||||||
Existing ISPmail tables (`virtual_domains`, `virtual_users`, `virtual_aliases`) are used for mail data.
|
Existing ISPmail tables (`virtual_domains`, `virtual_users`, `virtual_aliases`) are used for mail data.
|
||||||
|
|
||||||
|
### Database Access with sqlc
|
||||||
|
|
||||||
|
The app uses [sqlc](https://sqlc.dev/) for type-safe database access:
|
||||||
|
|
||||||
|
- SQL queries are defined in `backend/internal/db/queries/*.sql`
|
||||||
|
- Type-safe Go code is generated with `sqlc generate` into `backend/internal/db/sqlc/`
|
||||||
|
- **Do not edit files in `sqlc/`** - they are regenerated from queries
|
||||||
|
|
||||||
|
To regenerate after changing queries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && sqlc generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable SQL query logging in development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USE_EMBEDDED=false ./build/imc
|
||||||
|
```
|
||||||
|
|
||||||
## Systemd Service
|
## Systemd Service
|
||||||
|
|
||||||
Example service file at `/etc/systemd/system/imc-vibe.service`:
|
Example service file at `/etc/systemd/system/imc.service`:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=IMC Vibe Mail Admin
|
Description=IMC Mail Admin
|
||||||
After=network.target mariadb.service postfix.service
|
After=network.target mariadb.service postfix.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|
@ -122,7 +163,7 @@ Environment=DB_NAME=mailserver
|
||||||
Environment=JWT_SECRET=your_secret
|
Environment=JWT_SECRET=your_secret
|
||||||
Environment=BIND=0.0.0.0
|
Environment=BIND=0.0.0.0
|
||||||
Environment=PORT=8080
|
Environment=PORT=8080
|
||||||
ExecStart=/opt/imc-vibe/imc-vibe
|
ExecStart=/opt/imc/imc
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
@ -138,15 +179,48 @@ WantedBy=multi-user.target
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Tool Requirements
|
||||||
|
|
||||||
|
- [air](https://github.com/air-verse/air) for Go hot-reload: `go install github.com/air-verse/air@latest`
|
||||||
|
- [sqlc](https://sqlc.dev/) for SQL code generation: `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`
|
||||||
|
|
||||||
|
### Running Development Server
|
||||||
|
|
||||||
|
Start both frontend and backend with hot-reload:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts:
|
||||||
|
- **Frontend** on http://localhost:5173 (SvelteKit with HMR - frontend changes auto-reload)
|
||||||
|
- **Backend** on http://localhost:8080 (Go API with hot-reload)
|
||||||
|
|
||||||
|
**During development, access the app at http://localhost:5173** - the Vite dev server proxies `/api` requests to the backend and enables instant frontend updates.
|
||||||
|
|
||||||
|
### Running Servers Separately
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Backend only (port 8080, serves frontend from frontend/build/)
|
||||||
|
make dev-backend
|
||||||
|
|
||||||
|
# Frontend only (port 5173)
|
||||||
|
make dev-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
Build the binary into the build/ directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build frontend and backend
|
|
||||||
make build
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
# Run backend only (uses filesystem frontend)
|
Build only frontend or backend:
|
||||||
cd backend && go run ./cmd/server
|
|
||||||
|
|
||||||
# Run frontend dev server
|
```sh
|
||||||
cd frontend && bun run dev
|
make build-frontend
|
||||||
|
make build-backend
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
# Install air: go install github.com/air-verse/air@latest
|
# Install air: go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
bin = "./tmp/imc-vibe"
|
bin = "./tmp/imc"
|
||||||
cmd = "go build -o ./tmp/imc-vibe ./cmd/server"
|
cmd = "go build -o ./tmp/imc ./cmd/server"
|
||||||
stop_on_error = true
|
stop_on_error = true
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# Database
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_USER=mailadmin
|
|
||||||
DB_PASSWORD=your-password
|
|
||||||
DB_NAME=mailserver
|
|
||||||
|
|
||||||
# App
|
|
||||||
PORT=8080
|
|
||||||
USE_EMBEDDED=true
|
|
||||||
JWT_SECRET=your-random-secret-at-least-32-chars
|
|
||||||
TRUSTED_PROXIES=127.0.0.1
|
|
||||||
|
|
||||||
# Mail server paths
|
|
||||||
MAIL_DATA_DIR=/var/vmail
|
|
||||||
POSTQUEUE_PATH=/usr/sbin/postqueue
|
|
||||||
POSTSUPER_PATH=/usr/sbin/postsuper
|
|
||||||
DOVECOT_QUOTA_CMD=/usr/bin/doveadm
|
|
||||||
JOURNALCTL_PATH=/usr/bin/journalctl
|
|
||||||
RSPMAD_API=http://127.0.0.1:11334
|
|
||||||
|
|
@ -4,16 +4,22 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:embed
|
//go:embed all:embed
|
||||||
//go:embed all:embed/_app
|
|
||||||
var Files embed.FS
|
var Files embed.FS
|
||||||
|
|
||||||
func FrontendFileSystem() http.FileSystem {
|
func FrontendFileSystem() (http.FileSystem, string) {
|
||||||
return http.FS(Files)
|
if os.Getenv("USE_EMBEDDED") == "false" {
|
||||||
|
return http.Dir("../frontend/build"), ""
|
||||||
|
}
|
||||||
|
return http.FS(Files), "embed/"
|
||||||
}
|
}
|
||||||
|
|
||||||
func FrontendFS() fs.FS {
|
func FrontendFS() fs.FS {
|
||||||
|
if os.Getenv("USE_EMBEDDED") == "false" {
|
||||||
|
return os.DirFS("../frontend/build")
|
||||||
|
}
|
||||||
return Files
|
return Files
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Package main is the entry point for the imc-vibe mail server admin application.
|
// Package main is the entry point for the IMC mail server admin application.
|
||||||
// This single binary contains both the Go backend API and the embedded SvelteKit frontend.
|
// This single binary contains both the Go backend API and the embedded SvelteKit frontend.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context" // context for cancellation and timeouts
|
||||||
"crypto/rand" // cryptographically secure random number generator
|
"crypto/rand" // cryptographically secure random number generator
|
||||||
"flag" // standard library for parsing command-line flags
|
"flag" // standard library for parsing command-line flags
|
||||||
"fmt" // formatted I/O, used here for printing output
|
"fmt" // formatted I/O, used here for printing output
|
||||||
|
|
@ -11,11 +12,11 @@ import (
|
||||||
"os" // OS-level operations like reading command-line args
|
"os" // OS-level operations like reading command-line args
|
||||||
"strings" // string manipulation utilities
|
"strings" // string manipulation utilities
|
||||||
|
|
||||||
"github.com/gin-gonic/gin" // web framework
|
"git.workaround.org/chaas/imc/backend/internal/api" // HTTP API routing and handlers
|
||||||
"github.com/imc-vibe/backend/internal/api" // HTTP API routing and handlers
|
"git.workaround.org/chaas/imc/backend/internal/auth" // password hashing
|
||||||
"github.com/imc-vibe/backend/internal/auth" // password hashing
|
"git.workaround.org/chaas/imc/backend/internal/config" // configuration loading
|
||||||
"github.com/imc-vibe/backend/internal/config" // configuration loading
|
"git.workaround.org/chaas/imc/backend/internal/db" // database connection and operations
|
||||||
"github.com/imc-vibe/backend/internal/db" // database connection and operations
|
"github.com/gin-gonic/gin" // web framework
|
||||||
)
|
)
|
||||||
|
|
||||||
// main is the entry point of the application.
|
// main is the entry point of the application.
|
||||||
|
|
@ -24,7 +25,7 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
// Parse command-line flags.
|
// Parse command-line flags.
|
||||||
// Flags are optional arguments passed after the program name.
|
// Flags are optional arguments passed after the program name.
|
||||||
// Example: ./imc-vibe --bind=0.0.0.0 --port=8080
|
// Example: ./imc --bind=0.0.0.0 --port=8080
|
||||||
|
|
||||||
// --bind: The IP address the server should listen on.
|
// --bind: The IP address the server should listen on.
|
||||||
// 0.0.0.0 means listen on all network interfaces (accessible from other machines).
|
// 0.0.0.0 means listen on all network interfaces (accessible from other machines).
|
||||||
|
|
@ -69,14 +70,14 @@ func main() {
|
||||||
|
|
||||||
// Connect to the database (MariaDB/MySQL).
|
// Connect to the database (MariaDB/MySQL).
|
||||||
// The database stores both ISPmail data (virtual_users, virtual_domains, etc.)
|
// The database stores both ISPmail data (virtual_users, virtual_domains, etc.)
|
||||||
// and imc-vibe's own data (imc_users, imc_users2domains, etc.).
|
// and IMC's own data (imc_users, imc_users2domains, etc.).
|
||||||
database, err := db.Connect(cfg)
|
database, err := db.Connect(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to connect to database: %v", err) // Fatal = print and exit
|
log.Fatalf("Failed to connect to database: %v", err) // Fatal = print and exit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create database tables if they don't exist.
|
// Create database tables if they don't exist.
|
||||||
// This only creates imc-vibe's own tables, not the ISPmail tables.
|
// This only creates IMC's own tables, not the ISPmail tables.
|
||||||
if err := database.InitSchema(); err != nil {
|
if err := database.InitSchema(); err != nil {
|
||||||
log.Printf("Warning: Could not initialize schema: %v", err) // Non-fatal = continue
|
log.Printf("Warning: Could not initialize schema: %v", err) // Non-fatal = continue
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +90,23 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to hash password: %v", err)
|
log.Fatalf("Failed to hash password: %v", err)
|
||||||
}
|
}
|
||||||
if err := database.UpsertAdminUser("admin", hash); err != nil {
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Get or create a default domain for the admin user.
|
||||||
|
domains, err := database.GetAllDomains(ctx)
|
||||||
|
if err != nil || len(domains) == 0 {
|
||||||
|
// Create a default domain if none exist.
|
||||||
|
err := database.CreateDomain(ctx, "localhost")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create default domain: %v", err)
|
||||||
|
}
|
||||||
|
domains, err = database.GetAllDomains(ctx)
|
||||||
|
if err != nil || len(domains) == 0 {
|
||||||
|
log.Fatalf("Failed to get domain: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := database.UpsertAdminUser(ctx, "admin", hash, domains[0].ID); err != nil {
|
||||||
log.Fatalf("Failed to reset admin password: %v", err)
|
log.Fatalf("Failed to reset admin password: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Admin password reset successfully.\n")
|
fmt.Printf("Admin password reset successfully.\n")
|
||||||
|
|
@ -103,14 +120,20 @@ func main() {
|
||||||
|
|
||||||
// Get the embedded filesystem containing the frontend.
|
// Get the embedded filesystem containing the frontend.
|
||||||
// The frontend is compiled into the binary during build time.
|
// The frontend is compiled into the binary during build time.
|
||||||
frontendFS := FrontendFileSystem()
|
frontendFS, frontendPrefix := FrontendFileSystem()
|
||||||
|
|
||||||
// Set Gin to release mode for production.
|
// Set Gin mode based on environment.
|
||||||
// This disables debug logging and other development features.
|
// USE_EMBEDDED=false means development mode (e.g., via air hot-reloader).
|
||||||
gin.SetMode(gin.ReleaseMode)
|
// Release mode disables debug logging and other development features.
|
||||||
|
if os.Getenv("USE_EMBEDDED") == "false" {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new Gin engine (router).
|
// Create a new Gin engine (router).
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
|
engine.Use(gin.Logger()) // Log HTTP requests (shows method, path, status, latency)
|
||||||
engine.Use(gin.Recovery())
|
engine.Use(gin.Recovery())
|
||||||
|
|
||||||
// Register API routes from the router.
|
// Register API routes from the router.
|
||||||
|
|
@ -120,31 +143,31 @@ func main() {
|
||||||
// Set up SPA fallback for non-API routes.
|
// Set up SPA fallback for non-API routes.
|
||||||
// This must be the last route to catch all unmatched paths.
|
// This must be the last route to catch all unmatched paths.
|
||||||
engine.NoRoute(func(c *gin.Context) {
|
engine.NoRoute(func(c *gin.Context) {
|
||||||
path := c.Request.URL.Path
|
reqPath := c.Request.URL.Path
|
||||||
|
|
||||||
// If it's an API path, return 404.
|
// If it's an API path, return 404.
|
||||||
// The API router should have handled /api/* routes.
|
// The API router should have handled /api/* routes.
|
||||||
if strings.HasPrefix(path, "/api/") {
|
if strings.HasPrefix(reqPath, "/api/") {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve SvelteKit hashed assets at /_app/*
|
// Serve SvelteKit hashed assets at /_app/*
|
||||||
if strings.HasPrefix(path, "/_app/") {
|
if strings.HasPrefix(reqPath, "/_app/") {
|
||||||
assetPath := "embed/_app/" + strings.TrimPrefix(path, "/_app/")
|
filePath := frontendPrefix + "_app/" + strings.TrimPrefix(reqPath, "/_app/")
|
||||||
serveGinStaticFile(c, frontendFS, assetPath)
|
serveGinStaticFile(c, frontendFS, filePath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve favicon
|
// Serve favicon
|
||||||
if path == "/favicon.png" {
|
if reqPath == "/favicon.png" {
|
||||||
serveGinStaticFile(c, frontendFS, "embed/favicon.png")
|
serveGinStaticFile(c, frontendFS, frontendPrefix+"favicon.png")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all other paths, serve the SPA index.html.
|
// For all other paths, serve the SPA index.html.
|
||||||
// This allows client-side routing (e.g., /domains/example.org).
|
// This allows client-side routing (e.g., /domains/example.org).
|
||||||
serveGinStaticFile(c, frontendFS, "embed/index.html")
|
serveGinStaticFile(c, frontendFS, frontendPrefix+"index.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build the address string for binding: "IP:PORT"
|
// Build the address string for binding: "IP:PORT"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
module github.com/imc-vibe/backend
|
module git.workaround.org/chaas/imc/backend
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
gorm.io/driver/mysql v1.6.0
|
|
||||||
gorm.io/gorm v1.31.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -22,10 +21,7 @@ require (
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,6 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
|
@ -97,9 +93,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
|
||||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AliasHandler struct {
|
type AliasHandler struct {
|
||||||
|
|
@ -34,19 +35,19 @@ func (h *AliasHandler) List(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
canAccess, _ := h.db.CanAccessDomain(c.Request.Context(), uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
domain, err := h.db.GetDomainByName(c.Request.Context(), domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
aliases, err := h.db.GetAliasesByDomain(domain.ID)
|
aliases, err := h.db.GetAliasesByDomain(c.Request.Context(), domain.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "database error")
|
Error(c, http.StatusInternalServerError, "database error")
|
||||||
return
|
return
|
||||||
|
|
@ -68,13 +69,13 @@ func (h *AliasHandler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
canAccess, _ := h.db.CanAccessDomain(c.Request.Context(), uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
domain, err := h.db.GetDomainByName(c.Request.Context(), domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
|
|
@ -86,13 +87,52 @@ func (h *AliasHandler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alias, err := h.db.CreateAliasInDomain(req.Source, req.Destination, domain.ID)
|
// Validate source email format
|
||||||
|
if err := validateEmailLocalPart(req.Source); err != nil {
|
||||||
|
Error(c, http.StatusBadRequest, "source "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure source is in the correct domain
|
||||||
|
if !strings.HasSuffix(req.Source, "@"+domainName) {
|
||||||
|
Error(c, http.StatusBadRequest, "source must be in domain "+domainName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate destination - can be single email or comma-separated list
|
||||||
|
destinations := strings.Split(req.Destination, ",")
|
||||||
|
for _, dest := range destinations {
|
||||||
|
dest = strings.TrimSpace(dest)
|
||||||
|
if dest == "" {
|
||||||
|
Error(c, http.StatusBadRequest, "destination cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Destination doesn't need @domain validation since it can be external
|
||||||
|
if strings.Contains(dest, "@") {
|
||||||
|
if err := validateEmailLocalPart(dest); err != nil {
|
||||||
|
Error(c, http.StatusBadRequest, "destination "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate alias
|
||||||
|
_, err = h.db.GetAliasBySource(c.Request.Context(), req.Source)
|
||||||
|
if err == nil {
|
||||||
|
Error(c, http.StatusConflict, "alias already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "sql: no rows") {
|
||||||
|
// Only error if it's not "no rows" error
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.CreateAlias(c.Request.Context(), domain.ID, req.Source, req.Destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to create alias")
|
Error(c, http.StatusInternalServerError, "failed to create alias")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Created(c, alias)
|
Created(c, map[string]string{"message": "alias created"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AliasHandler) Delete(c *gin.Context) {
|
func (h *AliasHandler) Delete(c *gin.Context) {
|
||||||
|
|
@ -110,7 +150,7 @@ func (h *AliasHandler) Delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
canAccess, _ := h.db.CanAccessDomain(c.Request.Context(), uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
|
|
@ -122,7 +162,8 @@ func (h *AliasHandler) Delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.DeleteAlias(uint(id)); err != nil {
|
err = h.db.DeleteAlias(c.Request.Context(), uint32(id))
|
||||||
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to delete alias")
|
Error(c, http.StatusInternalServerError, "failed to delete alias")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/auth"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/auth"
|
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
"github.com/imc-vibe/backend/internal/mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxLoginAttempts = 5
|
const MaxLoginAttempts = 5
|
||||||
|
|
@ -17,15 +14,13 @@ const MaxLoginAttempts = 5
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
db *db.DB
|
db *db.DB
|
||||||
jwtManager *auth.JWTManager
|
jwtManager *auth.JWTManager
|
||||||
emailService *mail.EmailService
|
|
||||||
trustedProxies []string
|
trustedProxies []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, emailService *mail.EmailService, trustedProxies []string) *AuthHandler {
|
func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, trustedProxies []string) *AuthHandler {
|
||||||
return &AuthHandler{
|
return &AuthHandler{
|
||||||
db: database,
|
db: database,
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
emailService: emailService,
|
|
||||||
trustedProxies: trustedProxies,
|
trustedProxies: trustedProxies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,15 +35,10 @@ type ChangePasswordRequest struct {
|
||||||
NewPassword string `json:"newPassword" binding:"required,min=8"`
|
NewPassword string `json:"newPassword" binding:"required,min=8"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForgotPasswordRequest struct {
|
|
||||||
Identifier string `json:"identifier" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Domains []string `json:"domains"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) Login(c *gin.Context) {
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
|
@ -58,32 +48,13 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.cleanupOldAttempts()
|
user, err := h.db.GetImcUserByUsername(c.Request.Context(), req.Username)
|
||||||
|
if err != nil || !auth.CheckPassword(req.Password, user.PasswordHash) {
|
||||||
ip := h.getClientIP(c)
|
|
||||||
|
|
||||||
if isLockedOut(ip, req.Username, h.db) {
|
|
||||||
Error(c, http.StatusTooManyRequests, "too many failed attempts, try again later")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.db.GetImcUserByUsername(req.Username)
|
|
||||||
if err != nil || user == nil || !auth.CheckPassword(req.Password, user.PasswordHash) {
|
|
||||||
recordFailedAttempt(req.Username, ip, h.db)
|
|
||||||
Error(c, http.StatusUnauthorized, "invalid credentials")
|
Error(c, http.StatusUnauthorized, "invalid credentials")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clearFailedAttempts(req.Username, ip, h.db)
|
token, err := h.jwtManager.GenerateToken(uint(user.ID), user.Username, string(user.Role.ImcUsersRole), 24*time.Hour)
|
||||||
|
|
||||||
domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin")
|
|
||||||
|
|
||||||
domainNames := make([]string, len(domains))
|
|
||||||
for i, d := range domains {
|
|
||||||
domainNames[i] = d.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := h.jwtManager.GenerateToken(user.ID, user.Username, user.Role, 24*time.Hour)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to generate token")
|
Error(c, http.StatusInternalServerError, "failed to generate token")
|
||||||
return
|
return
|
||||||
|
|
@ -92,10 +63,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
Success(c, map[string]interface{}{
|
Success(c, map[string]interface{}{
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": UserResponse{
|
"user": UserResponse{
|
||||||
ID: user.ID,
|
ID: uint(user.ID),
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Role: user.Role,
|
Role: string(user.Role.ImcUsersRole),
|
||||||
Domains: domainNames,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -107,65 +77,16 @@ func (h *AuthHandler) Me(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.db.GetImcUserByID(authCtx.UserID)
|
user, err := h.db.GetImcUserByID(c.Request.Context(), uint32(authCtx.UserID))
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
Error(c, http.StatusNotFound, "user not found")
|
Error(c, http.StatusNotFound, "user not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin")
|
|
||||||
|
|
||||||
domainNames := make([]string, len(domains))
|
|
||||||
for i, d := range domains {
|
|
||||||
domainNames[i] = d.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
Success(c, UserResponse{
|
Success(c, UserResponse{
|
||||||
ID: user.ID,
|
ID: uint(user.ID),
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Role: user.Role,
|
Role: string(user.Role.ImcUsersRole),
|
||||||
Domains: domainNames,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|
||||||
var req ForgotPasswordRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
Error(c, http.StatusBadRequest, "invalid request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Identifier = strings.TrimSpace(strings.ToLower(req.Identifier))
|
|
||||||
if req.Identifier == "" {
|
|
||||||
Error(c, http.StatusBadRequest, "identifier required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.db.GetImcUserByUsername(req.Identifier)
|
|
||||||
if err != nil {
|
|
||||||
Success(c, map[string]string{
|
|
||||||
"message": "If the account exists, a password reset link will be sent",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token := mail.GenerateToken()
|
|
||||||
expiresAt := time.Now().Add(1 * time.Hour)
|
|
||||||
|
|
||||||
if err := h.db.CreatePasswordResetToken(user.ID, token, expiresAt); err != nil {
|
|
||||||
log.Printf("Failed to create password reset token: %v", err)
|
|
||||||
Error(c, http.StatusInternalServerError, "failed to create reset token")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.emailService.SendPasswordReset(user.Username, token); err != nil {
|
|
||||||
log.Printf("Failed to send password reset email: %v", err)
|
|
||||||
Error(c, http.StatusInternalServerError, "failed to send email")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Success(c, map[string]string{
|
|
||||||
"message": "If the account exists, a password reset link will be sent",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,11 +99,11 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
||||||
|
|
||||||
var req ChangePasswordRequest
|
var req ChangePasswordRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
Error(c, http.StatusBadRequest, "invalid request")
|
Error(c, http.StatusBadRequest, "invalid request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.db.GetImcUserByID(authCtx.UserID)
|
user, err := h.db.GetImcUserByID(c.Request.Context(), uint32(authCtx.UserID))
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
Error(c, http.StatusNotFound, "user not found")
|
Error(c, http.StatusNotFound, "user not found")
|
||||||
return
|
return
|
||||||
|
|
@ -199,7 +120,7 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.db.UpdateImcUserPassword(user.ID, newHash)
|
err = h.db.UpdateImcUserPassword(c.Request.Context(), user.ID, newHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to update password")
|
Error(c, http.StatusInternalServerError, "failed to update password")
|
||||||
return
|
return
|
||||||
|
|
@ -212,57 +133,6 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||||
Success(c, map[string]string{"message": "logged out"})
|
Success(c, map[string]string{"message": "logged out"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLockedOut(ip, identifier string, database *db.DB) bool {
|
|
||||||
var count int64
|
|
||||||
cutoff := time.Now().Add(-15 * time.Minute)
|
|
||||||
|
|
||||||
database.Model(&db.ImcLoginAttempt{}).
|
|
||||||
Where("ip_address = ? AND attempted_at > ? AND successful = false", ip, cutoff).
|
|
||||||
Count(&count)
|
|
||||||
|
|
||||||
if count >= MaxLoginAttempts {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
database.Model(&db.ImcLoginAttempt{}).
|
|
||||||
Where("email = ? AND attempted_at > ? AND successful = false", identifier, cutoff).
|
|
||||||
Count(&count)
|
|
||||||
|
|
||||||
return count >= MaxLoginAttempts
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordFailedAttempt(identifier, ip string, database *db.DB) {
|
|
||||||
database.Create(&db.ImcLoginAttempt{
|
|
||||||
Email: identifier,
|
|
||||||
IPAddress: ip,
|
|
||||||
Successful: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearFailedAttempts(identifier, ip string, database *db.DB) {
|
|
||||||
database.Model(&db.ImcLoginAttempt{}).
|
|
||||||
Where("email = ? OR ip_address = ?", identifier, ip).
|
|
||||||
Update("successful", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) cleanupOldAttempts() {
|
|
||||||
cutoff := time.Now().Add(-24 * time.Hour)
|
|
||||||
h.db.Where("attempted_at < ? AND successful = false", cutoff).Delete(&db.ImcLoginAttempt{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) getClientIP(c *gin.Context) string {
|
func (h *AuthHandler) getClientIP(c *gin.Context) string {
|
||||||
remoteIP := c.ClientIP()
|
return c.ClientIP()
|
||||||
|
|
||||||
if len(h.trustedProxies) > 0 {
|
|
||||||
for _, proxy := range h.trustedProxies {
|
|
||||||
if remoteIP == proxy {
|
|
||||||
forwarded := c.GetHeader("X-Forwarded-For")
|
|
||||||
if forwarded != "" {
|
|
||||||
return strings.Split(forwarded, ",")[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteIP
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DomainHandler struct {
|
type DomainHandler struct {
|
||||||
|
|
@ -22,13 +22,6 @@ type CreateDomainRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DomainPermissions struct {
|
|
||||||
DomainID uint `json:"domainId"`
|
|
||||||
DomainName string `json:"domainName"`
|
|
||||||
UserID uint `json:"userId"`
|
|
||||||
CanManage bool `json:"canManage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DomainHandler) List(c *gin.Context) {
|
func (h *DomainHandler) List(c *gin.Context) {
|
||||||
authCtx := GetAuthContext(c)
|
authCtx := GetAuthContext(c)
|
||||||
if authCtx == nil {
|
if authCtx == nil {
|
||||||
|
|
@ -37,7 +30,19 @@ func (h *DomainHandler) List(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin := authCtx.IsAdmin()
|
isAdmin := authCtx.IsAdmin()
|
||||||
domains, err := h.db.GetUserAccessibleDomains(authCtx.UserID, isAdmin)
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
if isAdmin {
|
||||||
|
domainStats, err := h.db.GetAllDomainsWithCounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
Error(c, http.StatusInternalServerError, "database error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Success(c, domainStats)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domains, err := h.db.GetUserAccessibleDomains(ctx, uint32(authCtx.UserID), isAdmin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "database error")
|
Error(c, http.StatusInternalServerError, "database error")
|
||||||
return
|
return
|
||||||
|
|
@ -45,9 +50,8 @@ func (h *DomainHandler) List(c *gin.Context) {
|
||||||
|
|
||||||
domainStats := make([]db.DomainStats, len(domains))
|
domainStats := make([]db.DomainStats, len(domains))
|
||||||
for i, d := range domains {
|
for i, d := range domains {
|
||||||
var userCount, aliasCount int64
|
userCount, _ := h.db.CountUsersByDomain(ctx, d.ID)
|
||||||
h.db.Model(&db.User{}).Where("domain_id = ?", d.ID).Count(&userCount)
|
aliasCount, _ := h.db.CountAliasesByDomain(ctx, d.ID)
|
||||||
h.db.Model(&db.Alias{}).Where("domain_id = ?", d.ID).Count(&aliasCount)
|
|
||||||
domainStats[i] = db.DomainStats{
|
domainStats[i] = db.DomainStats{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
|
|
@ -66,7 +70,8 @@ func (h *DomainHandler) Get(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
ctx := c.Request.Context()
|
||||||
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
|
|
@ -78,7 +83,7 @@ func (h *DomainHandler) Get(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
|
|
@ -106,19 +111,20 @@ func (h *DomainHandler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, err := h.db.GetDomainByName(name)
|
ctx := c.Request.Context()
|
||||||
if err == nil && existing != nil {
|
_, err := h.db.GetDomainByName(ctx, name)
|
||||||
|
if err == nil {
|
||||||
Error(c, http.StatusConflict, "domain already exists")
|
Error(c, http.StatusConflict, "domain already exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.CreateDomain(name)
|
err = h.db.CreateDomain(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to create domain")
|
Error(c, http.StatusInternalServerError, "failed to create domain")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Created(c, domain)
|
Created(c, map[string]string{"message": "domain created"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateDomainName(name string) error {
|
func validateDomainName(name string) error {
|
||||||
|
|
@ -174,13 +180,15 @@ func (h *DomainHandler) Delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
ctx := c.Request.Context()
|
||||||
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.DeleteDomain(domain.ID); err != nil {
|
err = h.db.DeleteDomain(ctx, domain.ID)
|
||||||
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to delete domain")
|
Error(c, http.StatusInternalServerError, "failed to delete domain")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -188,14 +196,16 @@ func (h *DomainHandler) Delete(c *gin.Context) {
|
||||||
NoContent(c)
|
NoContent(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DomainPermissions struct {
|
||||||
|
DomainID uint32 `json:"domainId"`
|
||||||
|
DomainName string `json:"domainName"`
|
||||||
|
UserID uint32 `json:"userId"`
|
||||||
|
CanManage bool `json:"canManage"`
|
||||||
|
}
|
||||||
|
|
||||||
func (h *DomainHandler) GetPermissions(c *gin.Context) {
|
func (h *DomainHandler) GetPermissions(c *gin.Context) {
|
||||||
authCtx := GetAuthContext(c)
|
authCtx := GetAuthContext(c)
|
||||||
if authCtx == nil {
|
if authCtx == nil || !authCtx.IsAdmin() {
|
||||||
Error(c, http.StatusUnauthorized, "authentication required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !authCtx.IsAdmin() {
|
|
||||||
Error(c, http.StatusForbidden, "admin access required")
|
Error(c, http.StatusForbidden, "admin access required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -206,13 +216,14 @@ func (h *DomainHandler) GetPermissions(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
ctx := c.Request.Context()
|
||||||
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := h.db.GetUsersForDomain(domain.ID)
|
users, err := h.db.GetUsersForDomain(ctx, domain.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "database error")
|
Error(c, http.StatusInternalServerError, "database error")
|
||||||
return
|
return
|
||||||
|
|
@ -244,21 +255,23 @@ func (h *DomainHandler) AddPermission(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
ctx := c.Request.Context()
|
||||||
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
UserID uint `json:"userId" binding:"required"`
|
UserID uint32 `json:"userId" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
Error(c, http.StatusBadRequest, "invalid request")
|
Error(c, http.StatusBadRequest, "invalid request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.AddUserToDomain(req.UserID, domain.ID); err != nil {
|
err = h.db.AddUserToDomain(ctx, req.UserID, domain.ID)
|
||||||
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to add user to domain")
|
Error(c, http.StatusInternalServerError, "failed to add user to domain")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +292,8 @@ func (h *DomainHandler) RemovePermission(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
ctx := c.Request.Context()
|
||||||
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
|
|
@ -292,7 +306,8 @@ func (h *DomainHandler) RemovePermission(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.RemoveUserFromDomain(uint(userID), domain.ID); err != nil {
|
err = h.db.RemoveUserFromDomain(ctx, uint32(userID), domain.ID)
|
||||||
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to remove user from domain")
|
Error(c, http.StatusInternalServerError, "failed to remove user from domain")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/mail"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogsHandler struct{}
|
type LogsHandler struct{}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/auth"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/auth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthContextKey is the key used to store auth context in gin.Context.
|
// AuthContextKey is the key used to store auth context in gin.Context.
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/mail"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueueHandler struct{}
|
type QueueHandler struct{}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatsHandler struct {
|
type StatsHandler struct {
|
||||||
|
|
@ -21,19 +21,21 @@ type Stats struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *StatsHandler) Get(c *gin.Context) {
|
func (h *StatsHandler) Get(c *gin.Context) {
|
||||||
domains, err := h.db.GetAllDomains()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
domains, err := h.db.GetAllDomains(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
domains = []db.DomainStats{}
|
domains = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := h.db.GetAllMailUsers()
|
users, err := h.db.GetAllMailUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
users = []db.User{}
|
users = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
aliases, err := h.db.GetAllAliases()
|
aliases, err := h.db.GetAllAliases(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aliases = []db.AliasWithDomain{}
|
aliases = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := Stats{
|
stats := Stats{
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/auth"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/mail"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
"github.com/imc-vibe/backend/internal/mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
|
|
@ -22,7 +23,7 @@ func NewUserHandler(database *db.DB) *UserHandler {
|
||||||
|
|
||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
Email string `json:"email" binding:"required"`
|
Email string `json:"email" binding:"required"`
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password"`
|
||||||
Quota int64 `json:"quota"`
|
Quota int64 `json:"quota"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ type UpdateUserRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserWithQuota struct {
|
type UserWithQuota struct {
|
||||||
ID uint `json:"id"`
|
ID uint32 `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Quota int64 `json:"quota"`
|
Quota int64 `json:"quota"`
|
||||||
UsedQuota *int64 `json:"usedQuota"`
|
UsedQuota *int64 `json:"usedQuota"`
|
||||||
|
|
@ -51,19 +52,20 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
ctx := c.Request.Context()
|
||||||
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := h.db.GetUsersByDomain(domain.ID)
|
users, err := h.db.GetUsersByDomain(ctx, domain.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "database error")
|
Error(c, http.StatusInternalServerError, "database error")
|
||||||
return
|
return
|
||||||
|
|
@ -71,16 +73,17 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||||
|
|
||||||
result := make([]UserWithQuota, len(users))
|
result := make([]UserWithQuota, len(users))
|
||||||
for i, user := range users {
|
for i, user := range users {
|
||||||
|
quota := user.Quota.Int64
|
||||||
result[i] = UserWithQuota{
|
result[i] = UserWithQuota{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Quota: user.Quota,
|
Quota: quota,
|
||||||
}
|
}
|
||||||
|
|
||||||
quota, err := mail.GetQuota(user.Email)
|
mailQuota, err := mail.GetQuota(user.Email)
|
||||||
if err == nil && quota != nil {
|
if err == nil && mailQuota != nil {
|
||||||
result[i].Quota = quota.Limit
|
result[i].Quota = mailQuota.Limit
|
||||||
result[i].UsedQuota = "a.Used
|
result[i].UsedQuota = &mailQuota.Used
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +105,8 @@ func (h *UserHandler) Get(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
ctx := c.Request.Context()
|
||||||
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
|
|
@ -114,7 +118,7 @@ func (h *UserHandler) Get(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.db.GetUserByID(uint(id))
|
user, err := h.db.GetUserByID(ctx, uint32(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "user not found")
|
Error(c, http.StatusNotFound, "user not found")
|
||||||
return
|
return
|
||||||
|
|
@ -136,13 +140,14 @@ func (h *UserHandler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
ctx := c.Request.Context()
|
||||||
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := h.db.GetDomainByName(domainName)
|
domain, err := h.db.GetDomainByName(ctx, domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "domain not found")
|
Error(c, http.StatusNotFound, "domain not found")
|
||||||
return
|
return
|
||||||
|
|
@ -154,28 +159,31 @@ func (h *UserHandler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email local part (before @)
|
|
||||||
if err := validateEmailLocalPart(req.Email); err != nil {
|
if err := validateEmailLocalPart(req.Email); err != nil {
|
||||||
Error(c, http.StatusBadRequest, err.Error())
|
Error(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, _ := h.db.GetUserByEmail(req.Email)
|
_, err = h.db.GetUserByEmail(ctx, req.Email)
|
||||||
if existing != nil {
|
if err == nil {
|
||||||
Error(c, http.StatusConflict, "user already exists")
|
Error(c, http.StatusConflict, "user already exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordHash := "{BLF-CRYPT}" + req.Password
|
password := req.Password
|
||||||
|
if password == "" {
|
||||||
|
password = auth.GenerateRandomPassword(16)
|
||||||
|
}
|
||||||
|
passwordHash := "{BLF-CRYPT}" + password
|
||||||
|
|
||||||
user, err := h.db.CreateUserInDomain(req.Email, passwordHash, req.Quota, domain.ID)
|
err = h.db.CreateUser(ctx, domain.ID, req.Email, passwordHash, req.Quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("CreateUserInDomain error: email=%s, domain=%s, err=%v", req.Email, domainName, err)
|
log.Printf("CreateUser error: email=%s, domain=%s, err=%v", req.Email, domainName, err)
|
||||||
Error(c, http.StatusInternalServerError, "failed to create user")
|
Error(c, http.StatusInternalServerError, "failed to create user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Created(c, user)
|
Created(c, map[string]interface{}{"message": "user created", "password": password})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) Update(c *gin.Context) {
|
func (h *UserHandler) Update(c *gin.Context) {
|
||||||
|
|
@ -193,7 +201,8 @@ func (h *UserHandler) Update(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
ctx := c.Request.Context()
|
||||||
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
|
|
@ -205,7 +214,7 @@ func (h *UserHandler) Update(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.db.GetUserByID(uint(id))
|
user, err := h.db.GetUserByID(ctx, uint32(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(c, http.StatusNotFound, "user not found")
|
Error(c, http.StatusNotFound, "user not found")
|
||||||
return
|
return
|
||||||
|
|
@ -219,14 +228,14 @@ func (h *UserHandler) Update(c *gin.Context) {
|
||||||
|
|
||||||
if req.Password != "" {
|
if req.Password != "" {
|
||||||
passwordHash := "{BLF-CRYPT}" + req.Password
|
passwordHash := "{BLF-CRYPT}" + req.Password
|
||||||
if err := h.db.UpdateUserPassword(user.ID, passwordHash); err != nil {
|
if err := h.db.UpdateUserPassword(ctx, user.ID, passwordHash); err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to update password")
|
Error(c, http.StatusInternalServerError, "failed to update password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Quota >= 0 {
|
if req.Quota >= 0 {
|
||||||
if err := h.db.UpdateUserQuota(user.ID, req.Quota); err != nil {
|
if err := h.db.UpdateUserQuota(ctx, user.ID, req.Quota); err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to update quota")
|
Error(c, http.StatusInternalServerError, "failed to update quota")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +259,8 @@ func (h *UserHandler) Delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
ctx := c.Request.Context()
|
||||||
|
canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin())
|
||||||
if !canAccess {
|
if !canAccess {
|
||||||
Error(c, http.StatusForbidden, "access denied")
|
Error(c, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
|
|
@ -262,7 +272,8 @@ func (h *UserHandler) Delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.DeleteUser(uint(id)); err != nil {
|
err = h.db.DeleteUser(ctx, uint32(id))
|
||||||
|
if err != nil {
|
||||||
Error(c, http.StatusInternalServerError, "failed to delete user")
|
Error(c, http.StatusInternalServerError, "failed to delete user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -270,23 +281,7 @@ func (h *UserHandler) Delete(c *gin.Context) {
|
||||||
NoContent(c)
|
NoContent(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) ListAll(c *gin.Context) {
|
var emailLocalPartRegex = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-=?^_~-]+$")
|
||||||
authCtx := GetAuthContext(c)
|
|
||||||
if authCtx == nil || !authCtx.IsAdmin() {
|
|
||||||
Error(c, http.StatusForbidden, "admin access required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := h.db.GetAllMailUsers()
|
|
||||||
if err != nil {
|
|
||||||
Error(c, http.StatusInternalServerError, "database error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Success(c, users)
|
|
||||||
}
|
|
||||||
|
|
||||||
var emailLocalPartRegex = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~-]+$")
|
|
||||||
|
|
||||||
func validateEmailLocalPart(email string) error {
|
func validateEmailLocalPart(email string) error {
|
||||||
parts := strings.Split(email, "@")
|
parts := strings.Split(email, "@")
|
||||||
|
|
@ -299,17 +294,14 @@ func validateEmailLocalPart(email string) error {
|
||||||
return &ValidationError{Message: "username must be between 1 and 64 characters"}
|
return &ValidationError{Message: "username must be between 1 and 64 characters"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 5321: local-part cannot start or end with a dot
|
|
||||||
if strings.HasPrefix(localPart, ".") || strings.HasSuffix(localPart, ".") {
|
if strings.HasPrefix(localPart, ".") || strings.HasSuffix(localPart, ".") {
|
||||||
return &ValidationError{Message: "username cannot start or end with a dot"}
|
return &ValidationError{Message: "username cannot start or end with a dot"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 5321: local-part cannot contain consecutive dots
|
|
||||||
if strings.Contains(localPart, "..") {
|
if strings.Contains(localPart, "..") {
|
||||||
return &ValidationError{Message: "username cannot contain consecutive dots"}
|
return &ValidationError{Message: "username cannot contain consecutive dots"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check valid characters (RFC 5321: letters, digits, and special chars !#$%&'*+/=?^_`{|}~-)
|
|
||||||
if !emailLocalPartRegex.MatchString(localPart) {
|
if !emailLocalPartRegex.MatchString(localPart) {
|
||||||
return &ValidationError{Message: "username contains invalid characters"}
|
return &ValidationError{Message: "username contains invalid characters"}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ package api
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/api/handlers"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/auth"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/config"
|
||||||
|
"git.workaround.org/chaas/imc/backend/internal/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/api/handlers"
|
|
||||||
"github.com/imc-vibe/backend/internal/auth"
|
|
||||||
"github.com/imc-vibe/backend/internal/config"
|
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
|
||||||
"github.com/imc-vibe/backend/internal/mail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
|
|
@ -23,11 +22,10 @@ type Router struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(database *db.DB, cfg *config.Config) *Router {
|
func New(database *db.DB, cfg *config.Config) *Router {
|
||||||
jwtManager := auth.NewJWTManager(cfg.JWTSecret, "imc-vibe")
|
jwtManager := auth.NewJWTManager(cfg.JWTSecret, "imc")
|
||||||
emailService := mail.NewEmailService(cfg)
|
|
||||||
|
|
||||||
r := &Router{
|
r := &Router{
|
||||||
authHandler: handlers.NewAuthHandler(database, jwtManager, emailService, cfg.TrustedProxies),
|
authHandler: handlers.NewAuthHandler(database, jwtManager, cfg.TrustedProxies),
|
||||||
domainHandler: handlers.NewDomainHandler(database),
|
domainHandler: handlers.NewDomainHandler(database),
|
||||||
userHandler: handlers.NewUserHandler(database),
|
userHandler: handlers.NewUserHandler(database),
|
||||||
aliasHandler: handlers.NewAliasHandler(database),
|
aliasHandler: handlers.NewAliasHandler(database),
|
||||||
|
|
@ -46,7 +44,6 @@ func (r *Router) RegisterRoutes(engine *gin.Engine) {
|
||||||
})
|
})
|
||||||
|
|
||||||
engine.POST("/api/auth/login", r.authHandler.Login)
|
engine.POST("/api/auth/login", r.authHandler.Login)
|
||||||
engine.POST("/api/auth/forgot", r.authHandler.ForgotPassword)
|
|
||||||
engine.POST("/api/auth/logout", r.authHandler.Logout)
|
engine.POST("/api/auth/logout", r.authHandler.Logout)
|
||||||
|
|
||||||
authGroup := engine.Group("/api")
|
authGroup := engine.Group("/api")
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors" // standard errors package
|
"crypto/rand" // cryptographically secure random number generator
|
||||||
"time" // time handling
|
"errors" // standard errors package
|
||||||
|
"log" // logging
|
||||||
|
"time" // time handling
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5" // JWT library for token handling
|
"github.com/golang-jwt/jwt/v5" // JWT library for token handling
|
||||||
"golang.org/x/crypto/bcrypt" // bcrypt for secure password hashing
|
"golang.org/x/crypto/bcrypt" // bcrypt for secure password hashing
|
||||||
|
|
@ -125,3 +127,16 @@ func CheckPassword(password, hash string) bool {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateRandomPassword creates a cryptographically secure random password.
|
||||||
|
func GenerateRandomPassword(length int) string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
||||||
|
result := make([]byte, length)
|
||||||
|
if _, err := rand.Read(result); err != nil {
|
||||||
|
log.Fatalf("Failed to generate random password: %v", err)
|
||||||
|
}
|
||||||
|
for i := range result {
|
||||||
|
result[i] = charset[int(result[i])%len(charset)]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,6 @@ type Config struct {
|
||||||
|
|
||||||
// Rspamd settings (spam filter)
|
// Rspamd settings (spam filter)
|
||||||
RspamdAPI string // URL of the Rspamd web interface
|
RspamdAPI string // URL of the Rspamd web interface
|
||||||
|
|
||||||
// SMTP settings (for sending password reset emails)
|
|
||||||
SMTPHost string // SMTP server hostname
|
|
||||||
SMTPPort string // SMTP server port (587 for submission, 465 for SMTPS)
|
|
||||||
SMTPUser string // SMTP username
|
|
||||||
SMTPPassword string // SMTP password
|
|
||||||
SMTPFrom string // From address in outgoing emails
|
|
||||||
|
|
||||||
// Application settings
|
|
||||||
BaseURL string // Base URL of this application (used for generating links in emails)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from environment variables and .env files.
|
// Load reads configuration from environment variables and .env files.
|
||||||
|
|
@ -98,16 +88,6 @@ func Load() *Config {
|
||||||
// External services
|
// External services
|
||||||
RspamdAPI: getEnv("RSPAMD_API", "http://127.0.0.1:11334"),
|
RspamdAPI: getEnv("RSPAMD_API", "http://127.0.0.1:11334"),
|
||||||
|
|
||||||
// SMTP settings (for password reset emails)
|
|
||||||
SMTPHost: getEnv("SMTP_HOST", "localhost"),
|
|
||||||
SMTPPort: getEnv("SMTP_PORT", "587"),
|
|
||||||
SMTPUser: getEnv("SMTP_USER", ""),
|
|
||||||
SMTPPassword: getEnv("SMTP_PASSWORD", ""),
|
|
||||||
SMTPFrom: getEnv("SMTP_FROM", "noreply@localhost"),
|
|
||||||
|
|
||||||
// Application URL (for generating links in emails)
|
|
||||||
BaseURL: getEnv("BASE_URL", "http://localhost:8080"),
|
|
||||||
|
|
||||||
// Trusted proxies (comma-separated IPs)
|
// Trusted proxies (comma-separated IPs)
|
||||||
TrustedProxies: parseTrustedProxies(getEnv("TRUSTED_PROXIES", "")),
|
TrustedProxies: parseTrustedProxies(getEnv("TRUSTED_PROXIES", "")),
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +112,9 @@ func (c *Config) Validate() error {
|
||||||
if len(c.JWTSecret) < 32 {
|
if len(c.JWTSecret) < 32 {
|
||||||
return fmt.Errorf("JWT_SECRET must be at least 32 characters long")
|
return fmt.Errorf("JWT_SECRET must be at least 32 characters long")
|
||||||
}
|
}
|
||||||
|
if c.JWTSecret == "your-secret-key-at-least-32-chars" {
|
||||||
|
return fmt.Errorf("JWT_SECRET must not be the default value")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package db
|
|
||||||
|
|
||||||
func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
|
|
||||||
var aliases []Alias
|
|
||||||
if err := d.Preload("Domain").Find(&aliases).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]AliasWithDomain, len(aliases))
|
|
||||||
for i, a := range aliases {
|
|
||||||
result[i] = AliasWithDomain{
|
|
||||||
Alias: a,
|
|
||||||
DomainName: "",
|
|
||||||
}
|
|
||||||
if a.Domain != nil {
|
|
||||||
result[i].DomainName = a.Domain.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
result = []AliasWithDomain{}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetAliasesByDomain(domainID uint) ([]Alias, error) {
|
|
||||||
var aliases []Alias
|
|
||||||
if err := d.Where("domain_id = ?", domainID).Find(&aliases).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if aliases == nil {
|
|
||||||
aliases = []Alias{}
|
|
||||||
}
|
|
||||||
return aliases, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetAliasByID(id uint) (*Alias, error) {
|
|
||||||
var alias Alias
|
|
||||||
if err := d.Preload("Domain").First(&alias, id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &alias, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateAlias(source, destination string) (*Alias, error) {
|
|
||||||
alias := Alias{
|
|
||||||
Source: source,
|
|
||||||
Destination: destination,
|
|
||||||
}
|
|
||||||
if err := d.Create(&alias).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &alias, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Alias, error) {
|
|
||||||
alias := Alias{
|
|
||||||
DomainID: domainID,
|
|
||||||
Source: source,
|
|
||||||
Destination: destination,
|
|
||||||
}
|
|
||||||
if err := d.Create(&alias).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &alias, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) DeleteAlias(id uint) error {
|
|
||||||
return d.Delete(&Alias{}, id).Error
|
|
||||||
}
|
|
||||||
|
|
@ -1,248 +1,90 @@
|
||||||
// Package db provides database access for the imc-vibe application.
|
// Package db provides database access for the IMC application.
|
||||||
// It uses GORM (Go ORM) for database operations.
|
// It uses sqlc for type-safe SQL queries.
|
||||||
//
|
|
||||||
// The database contains two sets of tables:
|
|
||||||
// 1. ISPmail tables (virtual_domains, virtual_users, virtual_aliases) - existing mail data
|
|
||||||
// 2. imc tables (imc_users, imc_users2domains, etc.) - application-specific data
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt" // formatted error messages
|
"database/sql"
|
||||||
"time" // time package for timestamps
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gorm.io/driver/mysql" // GORM MySQL driver
|
"git.workaround.org/chaas/imc/backend/internal/config"
|
||||||
"gorm.io/gorm" // GORM ORM library
|
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||||
"gorm.io/gorm/logger" // GORM logger configuration
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
"github.com/imc-vibe/backend/internal/config" // configuration
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DB wraps GORM's database connection with helper methods.
|
|
||||||
// This is the main database access point for the application.
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
*gorm.DB // Embeds GORM's DB type, giving us all GORM methods
|
*imcdb.Queries
|
||||||
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ISPmail Models (existing mail server tables)
|
|
||||||
// These tables are shared with the ISPmail system and must not be modified.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// Domain represents a mail domain (e.g., "example.org").
|
|
||||||
// This corresponds to the existing ISPmail virtual_domains table.
|
|
||||||
type Domain struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key, auto-increment
|
|
||||||
Name string `gorm:"uniqueIndex;size:50;not null" json:"name"` // Domain name, must be unique
|
|
||||||
CreatedAt time.Time `json:"createdAt"` // When the domain was added
|
|
||||||
Users []User `gorm:"foreignKey:DomainID" json:"users,omitempty"` // Mail users in this domain
|
|
||||||
Aliases []Alias `gorm:"foreignKey:DomainID" json:"aliases,omitempty"` // Aliases in this domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName tells GORM to use the existing ISPmail table name.
|
|
||||||
func (Domain) TableName() string { return "virtual_domains" }
|
|
||||||
|
|
||||||
// User represents a mail user (e.g., "user@example.org").
|
|
||||||
// This corresponds to the existing ISPmail virtual_users table.
|
|
||||||
type User struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
|
|
||||||
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"` // Full email address, must be unique
|
|
||||||
Password string `gorm:"size:150;not null" json:"-"` // Mailbox password (bcrypt hash, - means exclude from JSON)
|
|
||||||
Quota int64 `gorm:"default:0" json:"quota"` // Mailbox size limit in bytes (0 = default)
|
|
||||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName tells GORM to use the existing ISPmail table name.
|
|
||||||
func (User) TableName() string { return "virtual_users" }
|
|
||||||
|
|
||||||
// Alias represents an email alias/forwarding rule.
|
|
||||||
// Maps one email address (source) to another (destination).
|
|
||||||
// This corresponds to the existing ISPmail virtual_aliases table.
|
|
||||||
type Alias struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
|
|
||||||
Source string `gorm:"size:100;not null" json:"source"` // Original email address (alias)
|
|
||||||
Destination string `gorm:"size:100;not null" json:"destination"` // Forward-to email address
|
|
||||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName tells GORM to use the existing ISPmail table name.
|
|
||||||
func (Alias) TableName() string { return "virtual_aliases" }
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// imc-vibe Application Models (tables created by this app)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// ImcUser represents an admin user who can log into this application.
|
|
||||||
// This is separate from mail users - it's for the web admin interface.
|
|
||||||
type ImcUser struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
Username string `gorm:"uniqueIndex;size:100;not null" json:"username"` // Login username
|
|
||||||
PasswordHash string `gorm:"size:255;not null" json:"-"` // Bcrypt hash of password (- means exclude from JSON)
|
|
||||||
Role string `gorm:"type:enum('admin','user');default:'user'" json:"role"` // Role: admin or regular user
|
|
||||||
CreatedAt time.Time `json:"createdAt"` // When account was created
|
|
||||||
Domains []ImcUserDomain `gorm:"foreignKey:UserID" json:"domains,omitempty"` // Domains this user can access
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName specifies the database table name for ImcUser.
|
|
||||||
func (ImcUser) TableName() string { return "imc_users" }
|
|
||||||
|
|
||||||
// ImcUserDomain represents the many-to-many relationship between admin users and domains.
|
|
||||||
// An admin user can have access to multiple domains, and a domain can be accessed by multiple users.
|
|
||||||
type ImcUserDomain struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
UserID uint `gorm:"not null" json:"userId"` // Foreign key to ImcUser
|
|
||||||
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
|
|
||||||
CreatedAt time.Time `json:"createdAt"` // When access was granted
|
|
||||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain (for preloading)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName specifies the database table name for ImcUserDomain.
|
|
||||||
func (ImcUserDomain) TableName() string { return "imc_users2domains" }
|
|
||||||
|
|
||||||
// ImcLoginAttempt records login attempts for brute-force protection.
|
|
||||||
// Used to track failed login attempts and implement rate limiting.
|
|
||||||
type ImcLoginAttempt struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
Email string `gorm:"size:100;not null" json:"email"` // Email/username that was used
|
|
||||||
IPAddress string `gorm:"size:45;not null" json:"ipAddress"` // IP address of the requester (IPv6 compatible)
|
|
||||||
AttemptedAt time.Time `json:"attemptedAt"` // When the attempt occurred
|
|
||||||
Successful bool `gorm:"default:false" json:"successful"` // Whether the login succeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName specifies the database table name for ImcLoginAttempt.
|
|
||||||
func (ImcLoginAttempt) TableName() string { return "imc_login_attempts" }
|
|
||||||
|
|
||||||
// PasswordResetToken stores one-time tokens for password reset functionality.
|
|
||||||
type PasswordResetToken struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"` // Primary key
|
|
||||||
UserID uint `gorm:"not null" json:"userId"` // Foreign key to ImcUser
|
|
||||||
Token string `gorm:"size:64;not null;uniqueIndex" json:"token"` // The reset token (unique index for fast lookup)
|
|
||||||
ExpiresAt time.Time `gorm:"not null" json:"expiresAt"` // When this token expires
|
|
||||||
Used bool `gorm:"default:false" json:"used"` // Whether this token has been used
|
|
||||||
CreatedAt time.Time `json:"createdAt"` // When the token was created
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName specifies the database table name for PasswordResetToken.
|
|
||||||
func (PasswordResetToken) TableName() string { return "imc_password_reset_tokens" }
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Helper/View Models (not stored in database)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// DomainStats holds statistics about a domain (user count, alias count).
|
|
||||||
// Used for displaying dashboard information.
|
|
||||||
type DomainStats struct {
|
type DomainStats struct {
|
||||||
ID uint `json:"id"` // Domain ID
|
ID uint32 `json:"id"`
|
||||||
Name string `json:"name"` // Domain name
|
Name string `json:"name"`
|
||||||
UserCount int64 `json:"userCount"` // Number of mail users
|
UserCount int64 `json:"userCount"`
|
||||||
AliasCount int64 `json:"aliasCount"` // Number of aliases
|
AliasCount int64 `json:"aliasCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AliasWithDomain combines an alias with its domain name for API responses.
|
|
||||||
type AliasWithDomain struct {
|
type AliasWithDomain struct {
|
||||||
Alias // Embed Alias struct
|
imcdb.VirtualAlias
|
||||||
DomainName string `json:"domainName"` // Denormalized domain name for convenience
|
DomainName string `json:"domainName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserWithDomain combines a user with their domain name for API responses.
|
|
||||||
type UserWithDomain struct {
|
|
||||||
User // Embed User struct
|
|
||||||
DomainName string `json:"domainName"` // Denormalized domain name for convenience
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Database Connection
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// Connect establishes a connection to the MySQL/MariaDB database.
|
|
||||||
// cfg: configuration containing database credentials and connection settings.
|
|
||||||
// Returns: a DB wrapper around the GORM connection, or an error if connection fails.
|
|
||||||
func Connect(cfg *config.Config) (*DB, error) {
|
func Connect(cfg *config.Config) (*DB, error) {
|
||||||
// Build the Data Source Name (DSN) string.
|
|
||||||
// Format: username:password@protocol(address:port)/dbname?options
|
|
||||||
// parseTime=true: automatically convert time.Time to/from SQL DATETIME
|
|
||||||
// charset=utf8mb4: use full UTF-8 encoding (supports emojis, etc.)
|
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4",
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4",
|
||||||
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName)
|
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName)
|
||||||
|
|
||||||
// Open a connection to the database using GORM.
|
mysqlDB, err := sql.Open("mysql", dsn)
|
||||||
// We configure GORM to be silent (no auto-logging) to reduce noise.
|
|
||||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the underlying sql.DB object from GORM.
|
mysqlDB.SetMaxOpenConns(25)
|
||||||
// GORM wraps the standard database/sql package.
|
mysqlDB.SetMaxIdleConns(5)
|
||||||
sqlDB, err := db.DB()
|
mysqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
if err := mysqlDB.Ping(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure connection pool settings for performance.
|
return &DB{
|
||||||
// These settings help balance between connection reuse and resource usage.
|
Queries: imcdb.New(mysqlDB),
|
||||||
|
db: mysqlDB,
|
||||||
// SetMaxOpenConns: maximum number of open connections to the database.
|
}, nil
|
||||||
// Too few = slow requests, too many = database overload.
|
|
||||||
// 25 is a reasonable default for most applications.
|
|
||||||
sqlDB.SetMaxOpenConns(25)
|
|
||||||
|
|
||||||
// SetMaxIdleConns: maximum number of idle connections in the pool.
|
|
||||||
// Idle connections are kept open even when not in use.
|
|
||||||
// This avoids the overhead of opening new connections.
|
|
||||||
sqlDB.SetMaxIdleConns(5)
|
|
||||||
|
|
||||||
// SetConnMaxLifetime: maximum time a connection can be reused.
|
|
||||||
// Connections older than this are closed and replaced.
|
|
||||||
// This helps avoid issues with stale connections.
|
|
||||||
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
|
||||||
|
|
||||||
return &DB{db}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitSchema creates imc-vibe's database tables if they don't exist.
|
|
||||||
// This only creates the imc_* tables, not the ISPmail virtual_* tables.
|
|
||||||
// Called during application startup.
|
|
||||||
func (d *DB) InitSchema() error {
|
func (d *DB) InitSchema() error {
|
||||||
// Create imc_users table for admin accounts.
|
|
||||||
// This table stores admin users who can log into the web interface.
|
|
||||||
imcUsersSQL := `
|
imcUsersSQL := `
|
||||||
CREATE TABLE IF NOT EXISTS imc_users (
|
CREATE TABLE IF NOT EXISTS imc_users (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
username VARCHAR(100) NOT NULL UNIQUE,
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
role ENUM('admin','user') DEFAULT 'user',
|
role ENUM('admin','user') DEFAULT 'user',
|
||||||
|
domain_id INT UNSIGNED NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
INDEX idx_username (username),
|
INDEX idx_username (username),
|
||||||
INDEX idx_role (role)
|
INDEX idx_role (role),
|
||||||
|
INDEX idx_domain_id (domain_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
`
|
`
|
||||||
if err := d.Exec(imcUsersSQL).Error; err != nil {
|
if _, err := d.db.Exec(imcUsersSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create imc_login_attempts table for tracking login attempts.
|
|
||||||
// Used for brute-force protection (rate limiting).
|
|
||||||
loginAttemptsSQL := `
|
loginAttemptsSQL := `
|
||||||
CREATE TABLE IF NOT EXISTS imc_login_attempts (
|
CREATE TABLE IF NOT EXISTS imc_login_attempts (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
email VARCHAR(100) NOT NULL,
|
username VARCHAR(100) NOT NULL,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
successful BOOLEAN DEFAULT FALSE,
|
successful BOOLEAN DEFAULT FALSE,
|
||||||
INDEX idx_email_time (email, attempted_at),
|
INDEX idx_username_time (username, attempted_at),
|
||||||
INDEX idx_ip_time (ip_address, attempted_at)
|
INDEX idx_ip_time (ip_address, attempted_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
`
|
`
|
||||||
if err := d.Exec(loginAttemptsSQL).Error; err != nil {
|
if _, err := d.db.Exec(loginAttemptsSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create imc_users2domains table for user-domain permissions.
|
|
||||||
// This implements many-to-many: a user can access multiple domains.
|
|
||||||
users2DomainsSQL := `
|
users2DomainsSQL := `
|
||||||
CREATE TABLE IF NOT EXISTS imc_users2domains (
|
CREATE TABLE IF NOT EXISTS imc_users2domains (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|
@ -254,23 +96,13 @@ func (d *DB) InitSchema() error {
|
||||||
INDEX idx_domain_id (domain_id)
|
INDEX idx_domain_id (domain_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
`
|
`
|
||||||
if err := d.Exec(users2DomainsSQL).Error; err != nil {
|
if _, err := d.db.Exec(users2DomainsSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create imc_password_reset_tokens table for password reset functionality.
|
return nil
|
||||||
// Stores temporary tokens for resetting forgotten passwords.
|
}
|
||||||
resetTokensSQL := `
|
|
||||||
CREATE TABLE IF NOT EXISTS imc_password_reset_tokens (
|
func (d *DB) Close() error {
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
return d.db.Close()
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
token VARCHAR(64) NOT NULL UNIQUE,
|
|
||||||
expires_at DATETIME NOT NULL,
|
|
||||||
used BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_token (token),
|
|
||||||
INDEX idx_user_id (user_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
||||||
`
|
|
||||||
return d.Exec(resetTokensSQL).Error
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
package db
|
|
||||||
|
|
||||||
func (d *DB) GetAllDomains() ([]DomainStats, error) {
|
|
||||||
var domains []Domain
|
|
||||||
if err := d.Order("name ASC").Find(&domains).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := make([]DomainStats, len(domains))
|
|
||||||
for i, dom := range domains {
|
|
||||||
var userCount, aliasCount int64
|
|
||||||
d.Model(&User{}).Where("domain_id = ?", dom.ID).Count(&userCount)
|
|
||||||
d.Model(&Alias{}).Where("domain_id = ?", dom.ID).Count(&aliasCount)
|
|
||||||
|
|
||||||
stats[i] = DomainStats{
|
|
||||||
ID: dom.ID,
|
|
||||||
Name: dom.Name,
|
|
||||||
UserCount: userCount,
|
|
||||||
AliasCount: aliasCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats == nil {
|
|
||||||
stats = []DomainStats{}
|
|
||||||
}
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetDomainByID(id uint) (*Domain, error) {
|
|
||||||
var domain Domain
|
|
||||||
if err := d.Where("id = ?", id).First(&domain).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetDomainByName(name string) (*Domain, error) {
|
|
||||||
var domain Domain
|
|
||||||
if err := d.Where("name = ?", name).First(&domain).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateDomain(name string) (*Domain, error) {
|
|
||||||
domain := Domain{Name: name}
|
|
||||||
if err := d.Create(&domain).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) DeleteDomain(id uint) error {
|
|
||||||
tx := d.Begin()
|
|
||||||
defer func() { tx.Rollback() }()
|
|
||||||
|
|
||||||
if err := tx.Where("domain_id = ?", id).Delete(&User{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Where("domain_id = ?", id).Delete(&Alias{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Delete(&Domain{}, id).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit().Error
|
|
||||||
}
|
|
||||||
|
|
@ -1,228 +1,166 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
// =============================================================================
|
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||||
// Admin User (ImcUser) Operations
|
)
|
||||||
// These functions manage admin users who can log into the web interface.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// GetImcUserByUsername looks up an admin user by their username.
|
var ErrNotFound = errors.New("record not found")
|
||||||
// Returns the user with their associated domain permissions preloaded.
|
|
||||||
// username: the username to search for.
|
|
||||||
// Returns: the user and any error.
|
|
||||||
func (d *DB) GetImcUserByUsername(username string) (*ImcUser, error) {
|
|
||||||
var user ImcUser
|
|
||||||
// Preload loads related data (Domains) to avoid N+1 queries.
|
|
||||||
// Where creates a SQL WHERE clause, ? is a placeholder for the username.
|
|
||||||
if err := d.Preload("Domains.Domain").Where("username = ?", username).First(&user).Error; err != nil {
|
|
||||||
return nil, err // Return nil user and the error
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetImcUserByID looks up an admin user by their ID.
|
func (d *DB) GetImcUserByUsername(ctx context.Context, username string) (*imcdb.ImcUser, error) {
|
||||||
// Returns the user with their associated domain permissions preloaded.
|
user, err := d.Queries.GetImcUserByUsername(ctx, username)
|
||||||
// id: the user's primary key ID.
|
|
||||||
// Returns: the user and any error.
|
|
||||||
func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) {
|
|
||||||
var user ImcUser
|
|
||||||
// First(&user, id) looks up by primary key.
|
|
||||||
if err := d.Preload("Domains.Domain").First(&user, id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateImcUser creates a new admin user account.
|
|
||||||
// username: the login username (must be unique).
|
|
||||||
// passwordHash: the bcrypt hash of the password (NOT the plain password).
|
|
||||||
// role: "admin" or "user".
|
|
||||||
// Returns: the created user and any error.
|
|
||||||
func (d *DB) CreateImcUser(username, passwordHash, role string) (*ImcUser, error) {
|
|
||||||
user := ImcUser{
|
|
||||||
Username: username,
|
|
||||||
PasswordHash: passwordHash,
|
|
||||||
Role: role,
|
|
||||||
}
|
|
||||||
// Create inserts the record into the database.
|
|
||||||
if err := d.Create(&user).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateImcUserPassword updates the password hash for a user.
|
|
||||||
// id: the user's ID.
|
|
||||||
// passwordHash: the new bcrypt hash of the password.
|
|
||||||
func (d *DB) UpdateImcUserPassword(id uint, passwordHash string) error {
|
|
||||||
// Model specifies which table/struct to update.
|
|
||||||
// Where filters which rows to update.
|
|
||||||
// Update only changes the specified fields.
|
|
||||||
return d.Model(&ImcUser{}).Where("id = ?", id).Update("password_hash", passwordHash).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteImcUser removes an admin user from the database.
|
|
||||||
// id: the user's ID to delete.
|
|
||||||
func (d *DB) DeleteImcUser(id uint) error {
|
|
||||||
return d.Delete(&ImcUser{}, id).Error // Delete by primary key
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpsertAdminUser creates the admin user if it doesn't exist, or updates the password if it does.
|
|
||||||
func (d *DB) UpsertAdminUser(username, passwordHash string) error {
|
|
||||||
var user ImcUser
|
|
||||||
err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error
|
|
||||||
if err == nil {
|
|
||||||
return d.Model(&ImcUser{}).Where("id = ?", user.ID).Update("password_hash", passwordHash).Error
|
|
||||||
}
|
|
||||||
_, err = d.CreateImcUser(username, passwordHash, "admin")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Domain Access Control
|
|
||||||
// These functions manage which domains users can access.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// GetUserAccessibleDomains returns all domains a user can access.
|
|
||||||
// Admin users can access all domains; regular users only their assigned domains.
|
|
||||||
// userID: the ID of the user.
|
|
||||||
// isAdmin: whether the user has admin privileges.
|
|
||||||
// Returns: a list of accessible domains.
|
|
||||||
func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]Domain, error) {
|
|
||||||
// Admins get access to all domains.
|
|
||||||
if isAdmin {
|
|
||||||
var domains []Domain
|
|
||||||
if err := d.Order("name ASC").Find(&domains).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return domains, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular users query the imc_users2domains table.
|
|
||||||
// We use raw SQL with Joins because GORM's association methods
|
|
||||||
// don't handle our cross-database queries well.
|
|
||||||
var domains []Domain
|
|
||||||
err := d.Table("imc_users2domains"). // Query from this table
|
|
||||||
Select("virtual_domains.*"). // Select all columns from domains
|
|
||||||
Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). // Join to get domain details
|
|
||||||
Where("imc_users2domains.user_id = ?", userID). // Only this user's domains
|
|
||||||
Order("name ASC").
|
|
||||||
Find(&domains).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if domains == nil {
|
return &user, nil
|
||||||
domains = []Domain{} // Return empty slice, not nil
|
|
||||||
}
|
|
||||||
return domains, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanAccessDomain checks if a user can access a specific domain.
|
func (d *DB) GetImcUserByID(ctx context.Context, id uint32) (*imcdb.ImcUser, error) {
|
||||||
// userID: the ID of the user.
|
user, err := d.Queries.GetImcUserByID(ctx, id)
|
||||||
// domainName: the name of the domain (e.g., "example.org").
|
if err != nil {
|
||||||
// isAdmin: whether the user has admin privileges.
|
return nil, err
|
||||||
// Returns: true if the user can access the domain, false otherwise.
|
}
|
||||||
func (d *DB) CanAccessDomain(userID uint, domainName string, isAdmin bool) (bool, error) {
|
return &user, nil
|
||||||
// Admins can access all domains.
|
}
|
||||||
|
|
||||||
|
func (d *DB) CreateImcUser(ctx context.Context, username, passwordHash, role string, domainID uint32) error {
|
||||||
|
roleNull := imcdb.NullImcUsersRole{ImcUsersRole: imcdb.ImcUsersRole(role), Valid: true}
|
||||||
|
return d.Queries.CreateImcUser(ctx, imcdb.CreateImcUserParams{
|
||||||
|
Username: username,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
Role: roleNull,
|
||||||
|
DomainID: domainID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateImcUserPassword(ctx context.Context, id uint32, passwordHash string) error {
|
||||||
|
return d.Queries.UpdateImcUserPassword(ctx, imcdb.UpdateImcUserPasswordParams{
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
ID: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteImcUser(ctx context.Context, id uint32) error {
|
||||||
|
return d.Queries.DeleteImcUser(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpsertAdminUser(ctx context.Context, username, passwordHash string, domainID uint32) error {
|
||||||
|
user, err := d.Queries.GetImcUserByUsername(ctx, username)
|
||||||
|
if err == nil && user.ID != 0 {
|
||||||
|
return d.Queries.UpdateImcUserPassword(ctx, imcdb.UpdateImcUserPasswordParams{
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
ID: user.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return d.CreateImcUser(ctx, username, passwordHash, "admin", domainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetUserAccessibleDomains(ctx context.Context, userID uint32, isAdmin bool) ([]imcdb.VirtualDomain, error) {
|
||||||
|
if isAdmin {
|
||||||
|
return d.Queries.GetAllDomains(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := d.Queries.GetImcUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryDomain, err := d.Queries.GetDomainByID(ctx, user.DomainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalDomains, err := d.Queries.GetAdditionalDomainsForUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domainMap := make(map[uint32]imcdb.VirtualDomain)
|
||||||
|
domainMap[primaryDomain.ID] = primaryDomain
|
||||||
|
for _, d := range additionalDomains {
|
||||||
|
domainMap[d.ID] = d
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]imcdb.VirtualDomain, 0, len(domainMap))
|
||||||
|
for _, d := range domainMap {
|
||||||
|
result = append(result, d)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CanAccessDomain(ctx context.Context, userID uint32, domainName string, isAdmin bool) (bool, error) {
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count matching records - if count > 0, access is granted.
|
user, err := d.Queries.GetImcUserByID(ctx, userID)
|
||||||
var count int64
|
|
||||||
err := d.Table("imc_users2domains").
|
|
||||||
Select("COUNT(*)"). // Count matching rows
|
|
||||||
Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). // Join to domain table
|
|
||||||
Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName). // Match user and domain name
|
|
||||||
Count(&count).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return count > 0, nil // True if at least one match found
|
|
||||||
|
primaryDomain, err := d.Queries.GetDomainByID(ctx, user.DomainID)
|
||||||
|
if err == nil && primaryDomain.Name == domainName {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := d.Queries.IsUserInDomain(ctx, imcdb.IsUserInDomainParams{UserID: userID})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUserToDomain grants a user access to a domain.
|
func (d *DB) GetUsersForDomain(ctx context.Context, domainID uint32) ([]imcdb.ImcUser, error) {
|
||||||
// userID: the ID of the user.
|
users, err := d.Queries.GetUsersForDomain(ctx, domainID)
|
||||||
// domainID: the ID of the domain.
|
if err != nil {
|
||||||
func (d *DB) AddUserToDomain(userID, domainID uint) error {
|
return nil, err
|
||||||
ud := ImcUserDomain{
|
}
|
||||||
|
|
||||||
|
additionalUsers, err := d.Queries.GetUsersForDomainViaJoin(ctx, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := make(map[uint32]imcdb.ImcUser)
|
||||||
|
for _, u := range users {
|
||||||
|
userMap[u.ID] = u
|
||||||
|
}
|
||||||
|
for _, u := range additionalUsers {
|
||||||
|
if _, exists := userMap[u.ID]; !exists {
|
||||||
|
userMap[u.ID] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]imcdb.ImcUser, 0, len(userMap))
|
||||||
|
for _, u := range userMap {
|
||||||
|
result = append(result, u)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) AddUserToDomain(ctx context.Context, userID, domainID uint32) error {
|
||||||
|
return d.Queries.AddUserToDomain(ctx, imcdb.AddUserToDomainParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
DomainID: domainID,
|
DomainID: domainID,
|
||||||
}
|
})
|
||||||
return d.Create(&ud).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUserFromDomain revokes a user's access to a domain.
|
func (d *DB) RemoveUserFromDomain(ctx context.Context, userID, domainID uint32) error {
|
||||||
// userID: the ID of the user.
|
return d.Queries.RemoveUserFromDomain(ctx, imcdb.RemoveUserFromDomainParams{
|
||||||
// domainID: the ID of the domain.
|
UserID: userID,
|
||||||
func (d *DB) RemoveUserFromDomain(userID, domainID uint) error {
|
DomainID: domainID,
|
||||||
// Delete records matching both user_id and domain_id.
|
})
|
||||||
return d.Where("user_id = ? AND domain_id = ?", userID, domainID).Delete(&ImcUserDomain{}).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersForDomain returns all admin users who can access a specific domain.
|
func (d *DB) IsUserInDomain(ctx context.Context, userID, domainID uint32) (bool, error) {
|
||||||
// domainID: the ID of the domain.
|
count, err := d.Queries.IsUserInDomain(ctx, imcdb.IsUserInDomainParams{
|
||||||
// Returns: a list of users who can access this domain.
|
UserID: userID,
|
||||||
func (d *DB) GetUsersForDomain(domainID uint) ([]ImcUser, error) {
|
DomainID: domainID,
|
||||||
var users []ImcUser
|
})
|
||||||
err := d.Table("imc_users2domains").
|
|
||||||
Select("imc_users.*"). // Select all columns from imc_users
|
|
||||||
Joins("JOIN imc_users ON imc_users.id = imc_users2domains.user_id"). // Join to users table
|
|
||||||
Where("imc_users2domains.domain_id = ?", domainID).
|
|
||||||
Find(&users).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return false, err
|
||||||
}
|
}
|
||||||
if users == nil {
|
return count > 0, nil
|
||||||
users = []ImcUser{}
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUserInDomain checks if a user already has access to a domain.
|
|
||||||
// userID: the ID of the user.
|
|
||||||
// domainID: the ID of the domain.
|
|
||||||
// Returns: true if the user already has access, false otherwise.
|
|
||||||
func (d *DB) IsUserInDomain(userID, domainID uint) (bool, error) {
|
|
||||||
var count int64
|
|
||||||
err := d.Model(&ImcUserDomain{}).Where("user_id = ? AND domain_id = ?", userID, domainID).Count(&count).Error
|
|
||||||
return count > 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Password Reset Tokens
|
|
||||||
// These functions manage one-time tokens for password reset.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// CreatePasswordResetToken creates a new password reset token.
|
|
||||||
// userID: the ID of the user requesting password reset.
|
|
||||||
// token: the random token string.
|
|
||||||
// expiresAt: when this token becomes invalid.
|
|
||||||
func (d *DB) CreatePasswordResetToken(userID uint, token string, expiresAt time.Time) error {
|
|
||||||
resetToken := PasswordResetToken{
|
|
||||||
UserID: userID,
|
|
||||||
Token: token,
|
|
||||||
ExpiresAt: expiresAt,
|
|
||||||
}
|
|
||||||
return d.Create(&resetToken).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetValidPasswordResetToken looks up a valid (unused, not expired) reset token.
|
|
||||||
// token: the token string to look up.
|
|
||||||
// Returns: the token if valid, or an error if not found/expired/used.
|
|
||||||
func (d *DB) GetValidPasswordResetToken(token string) (*PasswordResetToken, error) {
|
|
||||||
var resetToken PasswordResetToken
|
|
||||||
// Check token exists, hasn't been used, and hasn't expired.
|
|
||||||
err := d.Where("token = ? AND used = false AND expires_at > ?", token, time.Now()).First(&resetToken).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &resetToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkResetTokenUsed marks a token as used (prevents reuse).
|
|
||||||
// token: the token string to mark as used.
|
|
||||||
func (d *DB) MarkResetTokenUsed(token string) error {
|
|
||||||
return d.Model(&PasswordResetToken{}).Where("token = ?", token).Update("used", true).Error
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
backend/internal/db/queries/aliases.sql
Normal file
20
backend/internal/db/queries/aliases.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- name: GetAllAliases :many
|
||||||
|
SELECT * FROM virtual_aliases;
|
||||||
|
|
||||||
|
-- name: GetAliasesByDomain :many
|
||||||
|
SELECT * FROM virtual_aliases WHERE domain_id = ?;
|
||||||
|
|
||||||
|
-- name: GetAliasByID :one
|
||||||
|
SELECT * FROM virtual_aliases WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetAliasBySource :one
|
||||||
|
SELECT * FROM virtual_aliases WHERE source = ?;
|
||||||
|
|
||||||
|
-- name: CreateAlias :exec
|
||||||
|
INSERT INTO virtual_aliases (domain_id, source, destination) VALUES (?, ?, ?);
|
||||||
|
|
||||||
|
-- name: DeleteAlias :exec
|
||||||
|
DELETE FROM virtual_aliases WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: CountAliasesByDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM virtual_aliases WHERE domain_id = ?;
|
||||||
26
backend/internal/db/queries/domains.sql
Normal file
26
backend/internal/db/queries/domains.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- name: GetAllDomains :many
|
||||||
|
SELECT * FROM virtual_domains ORDER BY name ASC;
|
||||||
|
|
||||||
|
-- name: GetDomainByID :one
|
||||||
|
SELECT * FROM virtual_domains WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetDomainByName :one
|
||||||
|
SELECT * FROM virtual_domains WHERE name = ?;
|
||||||
|
|
||||||
|
-- name: CreateDomain :exec
|
||||||
|
INSERT INTO virtual_domains (name) VALUES (?);
|
||||||
|
|
||||||
|
-- name: DeleteDomain :exec
|
||||||
|
DELETE FROM virtual_domains WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetAllDomainsWithCounts :many
|
||||||
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.name,
|
||||||
|
d.created_at,
|
||||||
|
COALESCE(u.user_count, 0) as user_count,
|
||||||
|
COALESCE(a.alias_count, 0) as alias_count
|
||||||
|
FROM virtual_domains d
|
||||||
|
LEFT JOIN (SELECT domain_id, COUNT(*) as user_count FROM virtual_users GROUP BY domain_id) u ON u.domain_id = d.id
|
||||||
|
LEFT JOIN (SELECT domain_id, COUNT(*) as alias_count FROM virtual_aliases GROUP BY domain_id) a ON a.domain_id = d.id
|
||||||
|
ORDER BY d.name ASC;
|
||||||
36
backend/internal/db/queries/imc_users.sql
Normal file
36
backend/internal/db/queries/imc_users.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- name: GetImcUserByUsername :one
|
||||||
|
SELECT * FROM imc_users WHERE username = ?;
|
||||||
|
|
||||||
|
-- name: GetImcUserByID :one
|
||||||
|
SELECT * FROM imc_users WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: CreateImcUser :exec
|
||||||
|
INSERT INTO imc_users (username, password_hash, role, domain_id) VALUES (?, ?, ?, ?);
|
||||||
|
|
||||||
|
-- name: UpdateImcUserPassword :exec
|
||||||
|
UPDATE imc_users SET password_hash = ? WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: DeleteImcUser :exec
|
||||||
|
DELETE FROM imc_users WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetUsersForDomain :many
|
||||||
|
SELECT imc_users.* FROM imc_users WHERE domain_id = ?;
|
||||||
|
|
||||||
|
-- name: GetUsersForDomainViaJoin :many
|
||||||
|
SELECT imc_users.* FROM imc_users
|
||||||
|
JOIN imc_users2domains ON imc_users.id = imc_users2domains.user_id
|
||||||
|
WHERE imc_users2domains.domain_id = ?;
|
||||||
|
|
||||||
|
-- name: AddUserToDomain :exec
|
||||||
|
INSERT INTO imc_users2domains (user_id, domain_id) VALUES (?, ?);
|
||||||
|
|
||||||
|
-- name: RemoveUserFromDomain :exec
|
||||||
|
DELETE FROM imc_users2domains WHERE user_id = ? AND domain_id = ?;
|
||||||
|
|
||||||
|
-- name: IsUserInDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM imc_users2domains WHERE user_id = ? AND domain_id = ?;
|
||||||
|
|
||||||
|
-- name: GetAdditionalDomainsForUser :many
|
||||||
|
SELECT virtual_domains.* FROM virtual_domains
|
||||||
|
JOIN imc_users2domains ON virtual_domains.id = imc_users2domains.domain_id
|
||||||
|
WHERE imc_users2domains.user_id = ?;
|
||||||
52
backend/internal/db/queries/schema.sql
Normal file
52
backend/internal/db/queries/schema.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-- ISPmail tables (existing)
|
||||||
|
CREATE TABLE IF NOT EXISTS virtual_domains (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS virtual_users (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
domain_id INT UNSIGNED NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(150) NOT NULL,
|
||||||
|
quota BIGINT DEFAULT 0,
|
||||||
|
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS virtual_aliases (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
domain_id INT UNSIGNED NOT NULL,
|
||||||
|
source VARCHAR(100) NOT NULL,
|
||||||
|
destination VARCHAR(100) NOT NULL,
|
||||||
|
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- IMC application tables
|
||||||
|
CREATE TABLE IF NOT EXISTS imc_users (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin','user') DEFAULT 'user',
|
||||||
|
domain_id INT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS imc_login_attempts (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL,
|
||||||
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
|
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
successful BOOLEAN DEFAULT FALSE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS imc_users2domains (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT UNSIGNED NOT NULL,
|
||||||
|
domain_id INT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY unique_user_domain (user_id, domain_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES imc_users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
26
backend/internal/db/queries/users.sql
Normal file
26
backend/internal/db/queries/users.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- name: GetAllMailUsers :many
|
||||||
|
SELECT * FROM virtual_users;
|
||||||
|
|
||||||
|
-- name: GetUsersByDomain :many
|
||||||
|
SELECT * FROM virtual_users WHERE domain_id = ?;
|
||||||
|
|
||||||
|
-- name: GetUserByID :one
|
||||||
|
SELECT * FROM virtual_users WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetUserByEmail :one
|
||||||
|
SELECT * FROM virtual_users WHERE email = ?;
|
||||||
|
|
||||||
|
-- name: CreateUser :exec
|
||||||
|
INSERT INTO virtual_users (domain_id, email, password, quota) VALUES (?, ?, ?, ?);
|
||||||
|
|
||||||
|
-- name: UpdateUserPassword :exec
|
||||||
|
UPDATE virtual_users SET password = ? WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: UpdateUserQuota :exec
|
||||||
|
UPDATE virtual_users SET quota = ? WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: DeleteUser :exec
|
||||||
|
DELETE FROM virtual_users WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: CountUsersByDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM virtual_users WHERE domain_id = ?;
|
||||||
141
backend/internal/db/sqlc/aliases.sql.go
Normal file
141
backend/internal/db/sqlc/aliases.sql.go
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: aliases.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countAliasesByDomain = `-- name: CountAliasesByDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM virtual_aliases WHERE domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountAliasesByDomain(ctx context.Context, domainID uint32) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countAliasesByDomain, domainID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAlias = `-- name: CreateAlias :exec
|
||||||
|
INSERT INTO virtual_aliases (domain_id, source, destination) VALUES (?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateAliasParams struct {
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Destination string `json:"destination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateAlias(ctx context.Context, arg CreateAliasParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createAlias, arg.DomainID, arg.Source, arg.Destination)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAlias = `-- name: DeleteAlias :exec
|
||||||
|
DELETE FROM virtual_aliases WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAlias(ctx context.Context, id uint32) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteAlias, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAliasByID = `-- name: GetAliasByID :one
|
||||||
|
SELECT id, domain_id, source, destination FROM virtual_aliases WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAliasByID(ctx context.Context, id uint32) (VirtualAlias, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAliasByID, id)
|
||||||
|
var i VirtualAlias
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Source,
|
||||||
|
&i.Destination,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAliasBySource = `-- name: GetAliasBySource :one
|
||||||
|
SELECT id, domain_id, source, destination FROM virtual_aliases WHERE source = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAliasBySource(ctx context.Context, source string) (VirtualAlias, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAliasBySource, source)
|
||||||
|
var i VirtualAlias
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Source,
|
||||||
|
&i.Destination,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAliasesByDomain = `-- name: GetAliasesByDomain :many
|
||||||
|
SELECT id, domain_id, source, destination FROM virtual_aliases WHERE domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAliasesByDomain(ctx context.Context, domainID uint32) ([]VirtualAlias, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAliasesByDomain, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualAlias
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualAlias
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Source,
|
||||||
|
&i.Destination,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllAliases = `-- name: GetAllAliases :many
|
||||||
|
SELECT id, domain_id, source, destination FROM virtual_aliases
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllAliases(ctx context.Context) ([]VirtualAlias, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllAliases)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualAlias
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualAlias
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Source,
|
||||||
|
&i.Destination,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
31
backend/internal/db/sqlc/db.go
Normal file
31
backend/internal/db/sqlc/db.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
128
backend/internal/db/sqlc/domains.sql.go
Normal file
128
backend/internal/db/sqlc/domains.sql.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: domains.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createDomain = `-- name: CreateDomain :exec
|
||||||
|
INSERT INTO virtual_domains (name) VALUES (?)
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CreateDomain(ctx context.Context, name string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createDomain, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDomain = `-- name: DeleteDomain :exec
|
||||||
|
DELETE FROM virtual_domains WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteDomain(ctx context.Context, id uint32) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteDomain, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllDomains = `-- name: GetAllDomains :many
|
||||||
|
SELECT id, name, created_at FROM virtual_domains ORDER BY name ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllDomains(ctx context.Context) ([]VirtualDomain, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllDomains)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualDomain
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualDomain
|
||||||
|
if err := rows.Scan(&i.ID, &i.Name, &i.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllDomainsWithCounts = `-- name: GetAllDomainsWithCounts :many
|
||||||
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.name,
|
||||||
|
d.created_at,
|
||||||
|
COALESCE(u.user_count, 0) as user_count,
|
||||||
|
COALESCE(a.alias_count, 0) as alias_count
|
||||||
|
FROM virtual_domains d
|
||||||
|
LEFT JOIN (SELECT domain_id, COUNT(*) as user_count FROM virtual_users GROUP BY domain_id) u ON u.domain_id = d.id
|
||||||
|
LEFT JOIN (SELECT domain_id, COUNT(*) as alias_count FROM virtual_aliases GROUP BY domain_id) a ON a.domain_id = d.id
|
||||||
|
ORDER BY d.name ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllDomainsWithCountsRow struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
UserCount int64 `json:"user_count"`
|
||||||
|
AliasCount int64 `json:"alias_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllDomainsWithCounts(ctx context.Context) ([]GetAllDomainsWithCountsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllDomainsWithCounts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllDomainsWithCountsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllDomainsWithCountsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UserCount,
|
||||||
|
&i.AliasCount,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDomainByID = `-- name: GetDomainByID :one
|
||||||
|
SELECT id, name, created_at FROM virtual_domains WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetDomainByID(ctx context.Context, id uint32) (VirtualDomain, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getDomainByID, id)
|
||||||
|
var i VirtualDomain
|
||||||
|
err := row.Scan(&i.ID, &i.Name, &i.CreatedAt)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDomainByName = `-- name: GetDomainByName :one
|
||||||
|
SELECT id, name, created_at FROM virtual_domains WHERE name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetDomainByName(ctx context.Context, name string) (VirtualDomain, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getDomainByName, name)
|
||||||
|
var i VirtualDomain
|
||||||
|
err := row.Scan(&i.ID, &i.Name, &i.CreatedAt)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
233
backend/internal/db/sqlc/imc_users.sql.go
Normal file
233
backend/internal/db/sqlc/imc_users.sql.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: imc_users.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const addUserToDomain = `-- name: AddUserToDomain :exec
|
||||||
|
INSERT INTO imc_users2domains (user_id, domain_id) VALUES (?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
type AddUserToDomainParams struct {
|
||||||
|
UserID uint32 `json:"user_id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AddUserToDomain(ctx context.Context, arg AddUserToDomainParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, addUserToDomain, arg.UserID, arg.DomainID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createImcUser = `-- name: CreateImcUser :exec
|
||||||
|
INSERT INTO imc_users (username, password_hash, role, domain_id) VALUES (?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateImcUserParams struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Role NullImcUsersRole `json:"role"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateImcUser(ctx context.Context, arg CreateImcUserParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createImcUser,
|
||||||
|
arg.Username,
|
||||||
|
arg.PasswordHash,
|
||||||
|
arg.Role,
|
||||||
|
arg.DomainID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteImcUser = `-- name: DeleteImcUser :exec
|
||||||
|
DELETE FROM imc_users WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteImcUser(ctx context.Context, id uint32) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteImcUser, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAdditionalDomainsForUser = `-- name: GetAdditionalDomainsForUser :many
|
||||||
|
SELECT virtual_domains.id, virtual_domains.name, virtual_domains.created_at FROM virtual_domains
|
||||||
|
JOIN imc_users2domains ON virtual_domains.id = imc_users2domains.domain_id
|
||||||
|
WHERE imc_users2domains.user_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAdditionalDomainsForUser(ctx context.Context, userID uint32) ([]VirtualDomain, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAdditionalDomainsForUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualDomain
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualDomain
|
||||||
|
if err := rows.Scan(&i.ID, &i.Name, &i.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImcUserByID = `-- name: GetImcUserByID :one
|
||||||
|
SELECT id, username, password_hash, role, domain_id, created_at FROM imc_users WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetImcUserByID(ctx context.Context, id uint32) (ImcUser, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getImcUserByID, id)
|
||||||
|
var i ImcUser
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.Role,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImcUserByUsername = `-- name: GetImcUserByUsername :one
|
||||||
|
SELECT id, username, password_hash, role, domain_id, created_at FROM imc_users WHERE username = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetImcUserByUsername(ctx context.Context, username string) (ImcUser, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getImcUserByUsername, username)
|
||||||
|
var i ImcUser
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.Role,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsersForDomain = `-- name: GetUsersForDomain :many
|
||||||
|
SELECT imc_users.id, imc_users.username, imc_users.password_hash, imc_users.role, imc_users.domain_id, imc_users.created_at FROM imc_users WHERE domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUsersForDomain(ctx context.Context, domainID uint32) ([]ImcUser, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUsersForDomain, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ImcUser
|
||||||
|
for rows.Next() {
|
||||||
|
var i ImcUser
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.Role,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsersForDomainViaJoin = `-- name: GetUsersForDomainViaJoin :many
|
||||||
|
SELECT imc_users.id, imc_users.username, imc_users.password_hash, imc_users.role, imc_users.domain_id, imc_users.created_at FROM imc_users
|
||||||
|
JOIN imc_users2domains ON imc_users.id = imc_users2domains.user_id
|
||||||
|
WHERE imc_users2domains.domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUsersForDomainViaJoin(ctx context.Context, domainID uint32) ([]ImcUser, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUsersForDomainViaJoin, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ImcUser
|
||||||
|
for rows.Next() {
|
||||||
|
var i ImcUser
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.Role,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserInDomain = `-- name: IsUserInDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM imc_users2domains WHERE user_id = ? AND domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type IsUserInDomainParams struct {
|
||||||
|
UserID uint32 `json:"user_id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) IsUserInDomain(ctx context.Context, arg IsUserInDomainParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, isUserInDomain, arg.UserID, arg.DomainID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUserFromDomain = `-- name: RemoveUserFromDomain :exec
|
||||||
|
DELETE FROM imc_users2domains WHERE user_id = ? AND domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type RemoveUserFromDomainParams struct {
|
||||||
|
UserID uint32 `json:"user_id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) RemoveUserFromDomain(ctx context.Context, arg RemoveUserFromDomainParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, removeUserFromDomain, arg.UserID, arg.DomainID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateImcUserPassword = `-- name: UpdateImcUserPassword :exec
|
||||||
|
UPDATE imc_users SET password_hash = ? WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateImcUserPasswordParams struct {
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateImcUserPassword(ctx context.Context, arg UpdateImcUserPasswordParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateImcUserPassword, arg.PasswordHash, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
98
backend/internal/db/sqlc/models.go
Normal file
98
backend/internal/db/sqlc/models.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImcUsersRole string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ImcUsersRoleAdmin ImcUsersRole = "admin"
|
||||||
|
ImcUsersRoleUser ImcUsersRole = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *ImcUsersRole) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = ImcUsersRole(s)
|
||||||
|
case string:
|
||||||
|
*e = ImcUsersRole(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for ImcUsersRole: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullImcUsersRole struct {
|
||||||
|
ImcUsersRole ImcUsersRole `json:"imc_users_role"`
|
||||||
|
Valid bool `json:"valid"` // Valid is true if ImcUsersRole is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullImcUsersRole) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.ImcUsersRole, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.ImcUsersRole.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullImcUsersRole) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.ImcUsersRole), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImcLoginAttempt struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
IpAddress string `json:"ip_address"`
|
||||||
|
AttemptedAt sql.NullTime `json:"attempted_at"`
|
||||||
|
Successful sql.NullBool `json:"successful"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImcUser struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Role NullImcUsersRole `json:"role"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImcUsers2domain struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
UserID uint32 `json:"user_id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualAlias struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Destination string `json:"destination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualDomain struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualUser struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Quota sql.NullInt64 `json:"quota"`
|
||||||
|
}
|
||||||
180
backend/internal/db/sqlc/users.sql.go
Normal file
180
backend/internal/db/sqlc/users.sql.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: users.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countUsersByDomain = `-- name: CountUsersByDomain :one
|
||||||
|
SELECT COUNT(*) as count FROM virtual_users WHERE domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountUsersByDomain(ctx context.Context, domainID uint32) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countUsersByDomain, domainID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = `-- name: CreateUser :exec
|
||||||
|
INSERT INTO virtual_users (domain_id, email, password, quota) VALUES (?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateUserParams struct {
|
||||||
|
DomainID uint32 `json:"domain_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Quota sql.NullInt64 `json:"quota"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createUser,
|
||||||
|
arg.DomainID,
|
||||||
|
arg.Email,
|
||||||
|
arg.Password,
|
||||||
|
arg.Quota,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUser = `-- name: DeleteUser :exec
|
||||||
|
DELETE FROM virtual_users WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteUser(ctx context.Context, id uint32) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteUser, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllMailUsers = `-- name: GetAllMailUsers :many
|
||||||
|
SELECT id, domain_id, email, password, quota FROM virtual_users
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllMailUsers(ctx context.Context) ([]VirtualUser, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllMailUsers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualUser
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualUser
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Password,
|
||||||
|
&i.Quota,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||||
|
SELECT id, domain_id, email, password, quota FROM virtual_users WHERE email = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (VirtualUser, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
|
||||||
|
var i VirtualUser
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Password,
|
||||||
|
&i.Quota,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
|
SELECT id, domain_id, email, password, quota FROM virtual_users WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByID(ctx context.Context, id uint32) (VirtualUser, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||||
|
var i VirtualUser
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Password,
|
||||||
|
&i.Quota,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsersByDomain = `-- name: GetUsersByDomain :many
|
||||||
|
SELECT id, domain_id, email, password, quota FROM virtual_users WHERE domain_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUsersByDomain(ctx context.Context, domainID uint32) ([]VirtualUser, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUsersByDomain, domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []VirtualUser
|
||||||
|
for rows.Next() {
|
||||||
|
var i VirtualUser
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.DomainID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Password,
|
||||||
|
&i.Quota,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
||||||
|
UPDATE virtual_users SET password = ? WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserPasswordParams struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateUserPassword, arg.Password, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserQuota = `-- name: UpdateUserQuota :exec
|
||||||
|
UPDATE virtual_users SET quota = ? WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserQuotaParams struct {
|
||||||
|
Quota sql.NullInt64 `json:"quota"`
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserQuota(ctx context.Context, arg UpdateUserQuotaParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateUserQuota, arg.Quota, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
package db
|
|
||||||
|
|
||||||
func (d *DB) GetAllMailUsers() ([]User, error) {
|
|
||||||
var users []User
|
|
||||||
if err := d.Preload("Domain").Find(&users).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if users == nil {
|
|
||||||
users = []User{}
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetUsersByDomain(domainID uint) ([]User, error) {
|
|
||||||
var users []User
|
|
||||||
if err := d.Where("domain_id = ?", domainID).Find(&users).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if users == nil {
|
|
||||||
users = []User{}
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetUserByID(id uint) (*User, error) {
|
|
||||||
var user User
|
|
||||||
if err := d.Preload("Domain").First(&user, id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) GetUserByEmail(email string) (*User, error) {
|
|
||||||
var user User
|
|
||||||
if err := d.Where("email = ?", email).First(&user).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateUser(email, passwordHash string, quota int64) (*User, error) {
|
|
||||||
user := User{
|
|
||||||
Email: email,
|
|
||||||
Password: passwordHash,
|
|
||||||
Quota: quota,
|
|
||||||
}
|
|
||||||
if err := d.Create(&user).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainID uint) (*User, error) {
|
|
||||||
user := User{
|
|
||||||
DomainID: domainID,
|
|
||||||
Email: email,
|
|
||||||
Password: passwordHash,
|
|
||||||
Quota: quota,
|
|
||||||
}
|
|
||||||
if err := d.Create(&user).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) UpdateUserPassword(id uint, passwordHash string) error {
|
|
||||||
return d.Model(&User{}).Where("id = ?", id).Update("password", passwordHash).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) UpdateUserQuota(id uint, quota int64) error {
|
|
||||||
return d.Model(&User{}).Where("id = ?", id).Update("quota", quota).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) DeleteUser(id uint) error {
|
|
||||||
return d.Delete(&User{}, id).Error
|
|
||||||
}
|
|
||||||
35
backend/internal/db/virtual_aliases.go
Normal file
35
backend/internal/db/virtual_aliases.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) GetAllAliases(ctx context.Context) ([]imcdb.VirtualAlias, error) {
|
||||||
|
return d.Queries.GetAllAliases(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetAliasesByDomain(ctx context.Context, domainID uint32) ([]imcdb.VirtualAlias, error) {
|
||||||
|
return d.Queries.GetAliasesByDomain(ctx, domainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetAliasByID(ctx context.Context, id uint32) (imcdb.VirtualAlias, error) {
|
||||||
|
return d.Queries.GetAliasByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetAliasBySource(ctx context.Context, source string) (imcdb.VirtualAlias, error) {
|
||||||
|
return d.Queries.GetAliasBySource(ctx, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CreateAlias(ctx context.Context, domainID uint32, source, destination string) error {
|
||||||
|
return d.Queries.CreateAlias(ctx, imcdb.CreateAliasParams{
|
||||||
|
DomainID: domainID,
|
||||||
|
Source: source,
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteAlias(ctx context.Context, id uint32) error {
|
||||||
|
return d.Queries.DeleteAlias(ctx, id)
|
||||||
|
}
|
||||||
53
backend/internal/db/virtual_domains.go
Normal file
53
backend/internal/db/virtual_domains.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) GetAllDomains(ctx context.Context) ([]imcdb.VirtualDomain, error) {
|
||||||
|
return d.Queries.GetAllDomains(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetDomainByID(ctx context.Context, id uint32) (imcdb.VirtualDomain, error) {
|
||||||
|
return d.Queries.GetDomainByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetDomainByName(ctx context.Context, name string) (imcdb.VirtualDomain, error) {
|
||||||
|
return d.Queries.GetDomainByName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CreateDomain(ctx context.Context, name string) error {
|
||||||
|
return d.Queries.CreateDomain(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteDomain(ctx context.Context, id uint32) error {
|
||||||
|
return d.Queries.DeleteDomain(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetAllDomainsWithCounts(ctx context.Context) ([]DomainStats, error) {
|
||||||
|
results, err := d.Queries.GetAllDomainsWithCounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := make([]DomainStats, len(results))
|
||||||
|
for i, r := range results {
|
||||||
|
stats[i] = DomainStats{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
UserCount: r.UserCount,
|
||||||
|
AliasCount: r.AliasCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CountUsersByDomain(ctx context.Context, domainID uint32) (int64, error) {
|
||||||
|
return d.Queries.CountUsersByDomain(ctx, domainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CountAliasesByDomain(ctx context.Context, domainID uint32) (int64, error) {
|
||||||
|
return d.Queries.CountAliasesByDomain(ctx, domainID)
|
||||||
|
}
|
||||||
54
backend/internal/db/virtual_users.go
Normal file
54
backend/internal/db/virtual_users.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) GetAllMailUsers(ctx context.Context) ([]imcdb.VirtualUser, error) {
|
||||||
|
return d.Queries.GetAllMailUsers(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetUsersByDomain(ctx context.Context, domainID uint32) ([]imcdb.VirtualUser, error) {
|
||||||
|
return d.Queries.GetUsersByDomain(ctx, domainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetUserByID(ctx context.Context, id uint32) (imcdb.VirtualUser, error) {
|
||||||
|
return d.Queries.GetUserByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetUserByEmail(ctx context.Context, email string) (imcdb.VirtualUser, error) {
|
||||||
|
return d.Queries.GetUserByEmail(ctx, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CreateUser(ctx context.Context, domainID uint32, email, passwordHash string, quota int64) error {
|
||||||
|
quotaNull := sql.NullInt64{Int64: quota, Valid: true}
|
||||||
|
return d.Queries.CreateUser(ctx, imcdb.CreateUserParams{
|
||||||
|
DomainID: domainID,
|
||||||
|
Email: email,
|
||||||
|
Password: passwordHash,
|
||||||
|
Quota: quotaNull,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateUserPassword(ctx context.Context, id uint32, passwordHash string) error {
|
||||||
|
return d.Queries.UpdateUserPassword(ctx, imcdb.UpdateUserPasswordParams{
|
||||||
|
Password: passwordHash,
|
||||||
|
ID: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateUserQuota(ctx context.Context, id uint32, quota int64) error {
|
||||||
|
quotaNull := sql.NullInt64{Int64: quota, Valid: true}
|
||||||
|
return d.Queries.UpdateUserQuota(ctx, imcdb.UpdateUserQuotaParams{
|
||||||
|
Quota: quotaNull,
|
||||||
|
ID: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteUser(ctx context.Context, id uint32) error {
|
||||||
|
return d.Queries.DeleteUser(ctx, id)
|
||||||
|
}
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
// Package mail provides email sending functionality for password reset emails.
|
|
||||||
// Uses Go's net/smtp package for sending emails via SMTP protocol.
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand" // cryptographically secure random number generator
|
|
||||||
"crypto/tls" // TLS/SSL encryption for secure email sending
|
|
||||||
"encoding/hex" // hex encoding for token generation
|
|
||||||
"fmt" // string formatting for URLs and messages
|
|
||||||
"net/smtp" // simple mail transfer protocol package
|
|
||||||
|
|
||||||
"github.com/imc-vibe/backend/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EmailService handles sending emails through SMTP.
|
|
||||||
// Stores SMTP configuration and provides methods for different email types.
|
|
||||||
type EmailService struct {
|
|
||||||
host string // SMTP server hostname (e.g., "smtp.example.com")
|
|
||||||
port string // SMTP server port (e.g., "587" for TLS, "465" for SSL)
|
|
||||||
username string // SMTP authentication username
|
|
||||||
password string // SMTP authentication password
|
|
||||||
from string // Sender email address (e.g., "noreply@example.com")
|
|
||||||
baseURL string // Base URL of the web application (for building reset links)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEmailService creates a new EmailService instance from configuration.
|
|
||||||
// cfg: application configuration containing SMTP settings and base URL.
|
|
||||||
func NewEmailService(cfg *config.Config) *EmailService {
|
|
||||||
return &EmailService{
|
|
||||||
host: cfg.SMTPHost,
|
|
||||||
port: cfg.SMTPPort,
|
|
||||||
username: cfg.SMTPUser,
|
|
||||||
password: cfg.SMTPPassword,
|
|
||||||
from: cfg.SMTPFrom,
|
|
||||||
baseURL: cfg.BaseURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendPasswordReset sends a password reset email to the user.
|
|
||||||
// to: recipient email address
|
|
||||||
// token: unique reset token that was generated and stored in the database
|
|
||||||
// The email contains a clickable link to reset the password.
|
|
||||||
func (s *EmailService) SendPasswordReset(to, token string) error {
|
|
||||||
// Build the reset URL with the token as a query parameter.
|
|
||||||
// Example: https://mail.example.com/auth/reset-password?token=abc123...
|
|
||||||
resetURL := fmt.Sprintf("%s/auth/reset-password?token=%s", s.baseURL, token)
|
|
||||||
|
|
||||||
// Compose the email subject and body.
|
|
||||||
subject := "Password Reset Request"
|
|
||||||
body := fmt.Sprintf(`Hi,
|
|
||||||
|
|
||||||
You requested a password reset for your account.
|
|
||||||
|
|
||||||
Click the link below to reset your password:
|
|
||||||
%s
|
|
||||||
|
|
||||||
This link will expire in 1 hour.
|
|
||||||
|
|
||||||
If you didn't request this, please ignore this email.
|
|
||||||
|
|
||||||
Best regards,
|
|
||||||
IMC Vibe Mail Server Admin
|
|
||||||
`, resetURL)
|
|
||||||
|
|
||||||
// Send the email using the send method.
|
|
||||||
return s.send(to, subject, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send is the internal method that actually sends the email via SMTP.
|
|
||||||
// Handles both port 587 (TLS) and port 465 (SSL) connections.
|
|
||||||
func (s *EmailService) send(to, subject, body string) error {
|
|
||||||
// Check if SMTP is configured. If not, return an error.
|
|
||||||
if s.host == "" || s.username == "" {
|
|
||||||
return fmt.Errorf("SMTP not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the server address in host:port format.
|
|
||||||
addr := fmt.Sprintf("%s:%s", s.host, s.port)
|
|
||||||
|
|
||||||
// Build the email message with headers.
|
|
||||||
// \r\n is the standard line ending for email headers and body.
|
|
||||||
msg := fmt.Sprintf("From: %s\r\n"+
|
|
||||||
"To: %s\r\n"+
|
|
||||||
"Subject: %s\r\n"+
|
|
||||||
"MIME-Version: 1.0\r\n"+
|
|
||||||
"Content-Type: text/plain; charset=\"UTF-8\"\r\n"+
|
|
||||||
"\r\n"+
|
|
||||||
"%s", s.from, to, subject, body)
|
|
||||||
|
|
||||||
// Port 465 uses implicit SSL/TLS, port 587 uses STARTTLS.
|
|
||||||
// Both are secure, but they work differently.
|
|
||||||
var auth smtp.Auth
|
|
||||||
if s.port == "465" {
|
|
||||||
// For port 465, we need to establish a TLS connection first
|
|
||||||
// before sending anything, then upgrade the connection.
|
|
||||||
err := s.sendWithTLS(addr, to, msg)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
// For other ports (usually 587), use STARTTLS which upgrades
|
|
||||||
// a plain connection to TLS after connecting.
|
|
||||||
auth = smtp.PlainAuth("", s.username, s.password, s.host)
|
|
||||||
err := smtp.SendMail(addr, auth, s.from, []string{to}, []byte(msg))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendWithTLS sends an email using an explicit TLS connection on port 465.
|
|
||||||
// Port 465 uses implicit TLS - the entire connection is encrypted from the start.
|
|
||||||
func (s *EmailService) sendWithTLS(addr, to, msg string) error {
|
|
||||||
// Create a TLS configuration for the secure connection.
|
|
||||||
// ServerName must match the SMTP server's certificate.
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
ServerName: s.host,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establish a TLS connection to the SMTP server.
|
|
||||||
// This creates an encrypted tunnel from the start.
|
|
||||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close() // Ensure connection is closed when we're done
|
|
||||||
|
|
||||||
// Create an SMTP client on top of the TLS connection.
|
|
||||||
client, err := smtp.NewClient(conn, s.host)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// Authenticate with the SMTP server using PLAIN authentication.
|
|
||||||
// The empty string "" is the identity (usually same as username).
|
|
||||||
auth := smtp.PlainAuth("", s.username, s.password, s.host)
|
|
||||||
if err := client.Auth(auth); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the sender (MAIL FROM command).
|
|
||||||
if err := client.Mail(s.from); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the recipient (RCPT TO command).
|
|
||||||
if err := client.Rcpt(to); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start sending the message body (DATA command).
|
|
||||||
w, err := client.Data()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the email body to the data stream.
|
|
||||||
_, err = w.Write([]byte(msg))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the data writer to finish the message.
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the QUIT command to gracefully close the connection.
|
|
||||||
return client.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateToken creates a cryptographically random token for password reset links.
|
|
||||||
// Returns a 64-character hexadecimal string (32 bytes encoded as hex).
|
|
||||||
// Uses crypto/rand for security-sensitive token generation.
|
|
||||||
func GenerateToken() string {
|
|
||||||
b := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
panic("crypto/rand failed: " + err.Error())
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateToken checks if a token has the correct format.
|
|
||||||
// Returns true if the token is exactly 64 characters of valid hex.
|
|
||||||
// This is a quick validation before checking the database.
|
|
||||||
func ValidateToken(token string) bool {
|
|
||||||
if len(token) != 64 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := hex.DecodeString(token)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
13
backend/sqlc.yaml
Normal file
13
backend/sqlc.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "mysql"
|
||||||
|
queries: "./internal/db/queries"
|
||||||
|
schema: "./internal/db/queries/schema.sql"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "db"
|
||||||
|
out: "./internal/db/sqlc"
|
||||||
|
sql_package: "database/sql"
|
||||||
|
emit_json_tags: true
|
||||||
|
emit_interface: false
|
||||||
|
emit_exact_table_names: false
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "imc-vibe-frontend",
|
"name": "imc-frontend",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.0",
|
"@sveltejs/adapter-static": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-heros": "^8.0.1",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
|
|
@ -79,55 +80,55 @@
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.1", "", { "os": "android", "cpu": "arm" }, "sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.1", "", { "os": "android", "cpu": "arm64" }, "sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg=="],
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg=="],
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ=="],
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g=="],
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.1", "", { "os": "linux", "cpu": "arm" }, "sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA=="],
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg=="],
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA=="],
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA=="],
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ=="],
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g=="],
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw=="],
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A=="],
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A=="],
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.1", "", { "os": "linux", "cpu": "none" }, "sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw=="],
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug=="],
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w=="],
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ=="],
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA=="],
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ=="],
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw=="],
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg=="],
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.1", "", { "os": "win32", "cpu": "x64" }, "sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw=="],
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
|
||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
|
@ -179,7 +180,7 @@
|
||||||
|
|
||||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
|
@ -211,7 +212,7 @@
|
||||||
|
|
||||||
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
|
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.322", "", {}, "sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||||
|
|
||||||
|
|
@ -279,7 +280,7 @@
|
||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.59.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.1", "@rollup/rollup-android-arm64": "4.59.1", "@rollup/rollup-darwin-arm64": "4.59.1", "@rollup/rollup-darwin-x64": "4.59.1", "@rollup/rollup-freebsd-arm64": "4.59.1", "@rollup/rollup-freebsd-x64": "4.59.1", "@rollup/rollup-linux-arm-gnueabihf": "4.59.1", "@rollup/rollup-linux-arm-musleabihf": "4.59.1", "@rollup/rollup-linux-arm64-gnu": "4.59.1", "@rollup/rollup-linux-arm64-musl": "4.59.1", "@rollup/rollup-linux-loong64-gnu": "4.59.1", "@rollup/rollup-linux-loong64-musl": "4.59.1", "@rollup/rollup-linux-ppc64-gnu": "4.59.1", "@rollup/rollup-linux-ppc64-musl": "4.59.1", "@rollup/rollup-linux-riscv64-gnu": "4.59.1", "@rollup/rollup-linux-riscv64-musl": "4.59.1", "@rollup/rollup-linux-s390x-gnu": "4.59.1", "@rollup/rollup-linux-x64-gnu": "4.59.1", "@rollup/rollup-linux-x64-musl": "4.59.1", "@rollup/rollup-openbsd-x64": "4.59.1", "@rollup/rollup-openharmony-arm64": "4.59.1", "@rollup/rollup-win32-arm64-msvc": "4.59.1", "@rollup/rollup-win32-ia32-msvc": "4.59.1", "@rollup/rollup-win32-x64-gnu": "4.59.1", "@rollup/rollup-win32-x64-msvc": "4.59.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA=="],
|
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||||
|
|
||||||
|
|
@ -287,11 +288,13 @@
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"svelte": ["svelte@5.54.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg=="],
|
"svelte": ["svelte@5.55.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw=="],
|
||||||
|
|
||||||
|
"svelte-heros": ["svelte-heros@8.0.1", "", { "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-67rDThxmdbjvJxKDqDUnkTsmyI5JtXcS0S0Ryg/EmTlHscFyJNgJyEARX5/xt3ONsZA0i6+dWnq+EX5jH9Wq1Q=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.1", "", {}, "sha512-b+u3CEM6FjDHru+nhUSjDofpWSBp2rINziJWgApm72wwGasQ/wKXftZe4tI2Y5HPv6OpzXSZHOFq87H4vfsgsw=="],
|
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
2
frontend/node_modules/@rollup/rollup-linux-x64-gnu/package.json
generated
vendored
2
frontend/node_modules/@rollup/rollup-linux-x64-gnu/package.json
generated
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rollup/rollup-linux-x64-gnu",
|
"name": "@rollup/rollup-linux-x64-gnu",
|
||||||
"version": "4.59.1",
|
"version": "4.60.0",
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
BIN
frontend/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node
generated
vendored
BIN
frontend/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node
generated
vendored
Binary file not shown.
2
frontend/node_modules/@rollup/rollup-linux-x64-musl/package.json
generated
vendored
2
frontend/node_modules/@rollup/rollup-linux-x64-musl/package.json
generated
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rollup/rollup-linux-x64-musl",
|
"name": "@rollup/rollup-linux-x64-musl",
|
||||||
"version": "4.59.1",
|
"version": "4.60.0",
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
BIN
frontend/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node
generated
vendored
BIN
frontend/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node
generated
vendored
Binary file not shown.
2
frontend/node_modules/@typescript-eslint/types/package.json
generated
vendored
2
frontend/node_modules/@typescript-eslint/types/package.json
generated
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@typescript-eslint/types",
|
"name": "@typescript-eslint/types",
|
||||||
"version": "8.57.1",
|
"version": "8.57.2",
|
||||||
"description": "Types for the TypeScript-ESTree AST spec",
|
"description": "Types for the TypeScript-ESTree AST spec",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
||||||
6
frontend/node_modules/rollup/dist/bin/rollup
generated
vendored
6
frontend/node_modules/rollup/dist/bin/rollup
generated
vendored
File diff suppressed because one or more lines are too long
4
frontend/node_modules/rollup/dist/es/getLogFilter.js
generated
vendored
4
frontend/node_modules/rollup/dist/es/getLogFilter.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/es/parseAst.js
generated
vendored
4
frontend/node_modules/rollup/dist/es/parseAst.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/es/rollup.js
generated
vendored
4
frontend/node_modules/rollup/dist/es/rollup.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
97
frontend/node_modules/rollup/dist/es/shared/node-entry.js
generated
vendored
97
frontend/node_modules/rollup/dist/es/shared/node-entry.js
generated
vendored
|
|
@ -1,13 +1,13 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
Released under the MIT License.
|
Released under the MIT License.
|
||||||
*/
|
*/
|
||||||
import { EMPTY_OBJECT, ExportDefaultDeclaration as ExportDefaultDeclaration$1, CallExpression as CallExpression$1, EMPTY_ARRAY, LOGLEVEL_WARN, logUnusedExternalImports, ANNOTATION_KEY, INVALID_ANNOTATION_KEY, ExpressionStatement as ExpressionStatement$1, AwaitExpression as AwaitExpression$1, MemberExpression as MemberExpression$1, Identifier as Identifier$1, FunctionExpression as FunctionExpression$1, ArrowFunctionExpression as ArrowFunctionExpression$1, ObjectExpression as ObjectExpression$1, Property as Property$1, Program as Program$1, logIllegalImportReassignment, BLANK, logRedeclarationError, StaticBlock as StaticBlock$1, CatchClause as CatchClause$1, logDuplicateArgumentNameError, logModuleLevelDirective, ReturnStatement as ReturnStatement$1, VariableDeclarator as VariableDeclarator$1, logMissingExport, normalize, getImportPath, logMissingNodeBuiltins, logReservedNamespace, error, logIllegalIdentifierAsName, logMissingNameOptionForIifeExport, logMissingNameOptionForUmdExport, RestElement as RestElement$1, logConstVariableReassignError, EMPTY_SET, logCannotCallNamespace, logEval, BlockStatement as BlockStatement$1, getRollupError, logModuleParseError, logParseError, LOGLEVEL_INFO, logFirstSideEffect, locate, logInvalidAnnotation, logThisIsUndefined, getAstBuffer, convertAnnotations, FIXED_STRINGS, convertNode as convertNode$1, logImportAttributeIsInvalid, logImportOptionsAreInvalid, logMissingEntryExport, logSyntheticNamedExportsNeedNamespaceExport, logDuplicateExportError, logInvalidSourcemapForError, augmentCodeLocation, logInconsistentImportAttributes, logMissingJsxExport, logNamespaceConflict, logAmbiguousExternalNamespaces, logShimmedExport, parseAst, TemplateLiteral as TemplateLiteral$1, Literal as Literal$1, logCircularReexport, logInvalidFormatForTopLevelAwait, logAddonNotGenerated, logIncompatibleExportOptionValue, logMixedExport, logFailedValidation, isPathFragment, logCyclicCrossChunkReexport, getAliasName, logUnexpectedNamedImport, isAbsolute as isAbsolute$1, relative as relative$1, logUnexpectedNamespaceReexport, logEmptyChunk, logMissingGlobalName, logOptimizeChunkStatus, logSourcemapBroken, logConflictingSourcemapSources, logChunkInvalid, logInvalidOption, URL_OUTPUT_FORMAT, URL_OUTPUT_DIR, URL_OUTPUT_SOURCEMAPFILE, URL_OUTPUT_AMD_ID, logFileNameOutsideOutputDirectory, logCannotAssignModuleToChunk, logCircularChunk, logUnknownOption, printQuotedStringList, LOGLEVEL_ERROR, logLevelPriority, LOGLEVEL_DEBUG, logAnonymousPluginCache, logDuplicatePluginName, logInvalidSetAssetSourceCall, logPluginError, logNoTransformMapOrAstWithoutCode, warnDeprecation, URL_TRANSFORM, relativeId, logBadLoader, logInternalIdCannotBeExternal, isRelative, logUnresolvedImport, logUnresolvedImportTreatedAsExternal, logExternalSyntheticExports, logUnresolvedEntry, logUnresolvedImplicitDependant, logExternalModulesCannotBeIncludedInManualChunks, logEntryCannotBeExternal, logImplicitDependantCannotBeExternal, logExternalModulesCannotBeTransformedToModules, URL_LOAD, logNoAssetSourceSet, logFileReferenceIdNotFoundForFilename, logAssetReferenceIdNotFoundForSetSource, logAssetSourceAlreadySet, logInvalidRollupPhaseForChunkEmission, logChunkNotGeneratedForFileName, logAssetNotFinalisedForFileName, logFileNameConflict, URL_GENERATEBUNDLE, logInvalidLogPosition, logInputHookInOutputPlugin, logInvalidAddonPluginHook, logInvalidFunctionPluginHook, logImplicitDependantIsNotIncluded, logCircularDependency, augmentLogMessage, URL_JSX, URL_TREESHAKE, URL_TREESHAKE_MODULESIDEEFFECTS, URL_OUTPUT_INLINEDYNAMICIMPORTS, URL_PRESERVEENTRYSIGNATURES, URL_OUTPUT_GENERATEDCODE, isValidUrl, addTrailingSlashIfMissed, URL_OUTPUT_SOURCEMAPBASEURL, URL_OUTPUT_MANUALCHUNKS, logInvalidExportOptionValue, URL_OUTPUT_AMD_BASEPATH, URL_OUTPUT_INTEROP, URL_OUTPUT_EXTERNALIMPORTATTRIBUTES, logAlreadyClosed, logMissingFileOrDirOption, logCannotEmitFromOptionsHook, URL_WATCH } from './parseAst.js';
|
import { EMPTY_OBJECT, ExportDefaultDeclaration as ExportDefaultDeclaration$1, CallExpression as CallExpression$1, EMPTY_ARRAY, LOGLEVEL_WARN, logUnusedExternalImports, ANNOTATION_KEY, INVALID_ANNOTATION_KEY, ExpressionStatement as ExpressionStatement$1, AwaitExpression as AwaitExpression$1, MemberExpression as MemberExpression$1, Identifier as Identifier$1, FunctionExpression as FunctionExpression$1, ArrowFunctionExpression as ArrowFunctionExpression$1, ObjectExpression as ObjectExpression$1, Property as Property$1, Program as Program$1, logIllegalImportReassignment, BLANK, logRedeclarationError, StaticBlock as StaticBlock$1, CatchClause as CatchClause$1, logDuplicateArgumentNameError, logModuleLevelDirective, ReturnStatement as ReturnStatement$1, VariableDeclarator as VariableDeclarator$1, logMissingExport, normalize, getImportPath, error, logSourcePhaseFormatUnsupported, logMissingNodeBuiltins, logReservedNamespace, logIllegalIdentifierAsName, logMissingNameOptionForIifeExport, logMissingNameOptionForUmdExport, RestElement as RestElement$1, logConstVariableReassignError, EMPTY_SET, logCannotCallNamespace, logEval, BlockStatement as BlockStatement$1, getRollupError, logModuleParseError, logParseError, LOGLEVEL_INFO, logFirstSideEffect, locate, logInvalidAnnotation, logThisIsUndefined, getAstBuffer, convertAnnotations, FIXED_STRINGS, convertNode as convertNode$1, logImportAttributeIsInvalid, logImportOptionsAreInvalid, logMissingEntryExport, logSyntheticNamedExportsNeedNamespaceExport, logDuplicateExportError, logInvalidSourcemapForError, augmentCodeLocation, logInconsistentImportAttributes, logMissingJsxExport, logNamespaceConflict, logAmbiguousExternalNamespaces, logShimmedExport, parseAst, TemplateLiteral as TemplateLiteral$1, Literal as Literal$1, logCircularReexport, logInvalidFormatForTopLevelAwait, logAddonNotGenerated, logIncompatibleExportOptionValue, logMixedExport, logFailedValidation, isPathFragment, logCyclicCrossChunkReexport, getAliasName, logUnexpectedNamedImport, isAbsolute as isAbsolute$1, relative as relative$1, logUnexpectedNamespaceReexport, logEmptyChunk, logMissingGlobalName, logOptimizeChunkStatus, logSourcemapBroken, logConflictingSourcemapSources, logChunkInvalid, logInvalidOption, URL_OUTPUT_FORMAT, URL_OUTPUT_DIR, URL_OUTPUT_SOURCEMAPFILE, URL_OUTPUT_AMD_ID, logFileNameOutsideOutputDirectory, logCannotAssignModuleToChunk, logCircularChunk, logUnknownOption, printQuotedStringList, LOGLEVEL_ERROR, logLevelPriority, LOGLEVEL_DEBUG, logAnonymousPluginCache, logDuplicatePluginName, logInvalidSetAssetSourceCall, logPluginError, logNoTransformMapOrAstWithoutCode, warnDeprecation, URL_TRANSFORM, relativeId, logBadLoader, logNonExternalSourcePhaseImport, logInternalIdCannotBeExternal, isRelative, logUnresolvedImport, logUnresolvedImportTreatedAsExternal, logExternalSyntheticExports, logUnresolvedEntry, logUnresolvedImplicitDependant, logExternalModulesCannotBeIncludedInManualChunks, logEntryCannotBeExternal, logImplicitDependantCannotBeExternal, logExternalModulesCannotBeTransformedToModules, URL_LOAD, logNoAssetSourceSet, logFileReferenceIdNotFoundForFilename, logAssetReferenceIdNotFoundForSetSource, logAssetSourceAlreadySet, logInvalidRollupPhaseForChunkEmission, logChunkNotGeneratedForFileName, logAssetNotFinalisedForFileName, logFileNameConflict, URL_GENERATEBUNDLE, logInvalidLogPosition, logInputHookInOutputPlugin, logInvalidAddonPluginHook, logInvalidFunctionPluginHook, logImplicitDependantIsNotIncluded, logCircularDependency, augmentLogMessage, URL_JSX, URL_TREESHAKE, URL_TREESHAKE_MODULESIDEEFFECTS, URL_OUTPUT_INLINEDYNAMICIMPORTS, URL_PRESERVEENTRYSIGNATURES, URL_OUTPUT_GENERATEDCODE, isValidUrl, addTrailingSlashIfMissed, URL_OUTPUT_SOURCEMAPBASEURL, URL_OUTPUT_MANUALCHUNKS, logInvalidExportOptionValue, URL_OUTPUT_AMD_BASEPATH, URL_OUTPUT_INTEROP, URL_OUTPUT_EXTERNALIMPORTATTRIBUTES, logAlreadyClosed, logMissingFileOrDirOption, logCannotEmitFromOptionsHook, URL_WATCH } from './parseAst.js';
|
||||||
import { relative, dirname, basename, extname, resolve as resolve$1, join } from 'node:path';
|
import { relative, dirname, basename, extname, resolve as resolve$1, join } from 'node:path';
|
||||||
import { posix, isAbsolute, resolve, win32 } from 'path';
|
import { posix, isAbsolute, resolve, win32 } from 'path';
|
||||||
import { parseAsync, xxhashBase16, xxhashBase64Url, xxhashBase36 } from '../../native.js';
|
import { parseAsync, xxhashBase16, xxhashBase64Url, xxhashBase36 } from '../../native.js';
|
||||||
|
|
@ -27,7 +27,7 @@ function _mergeNamespaces(n, m) {
|
||||||
return Object.defineProperty(n, Symbol.toStringTag, { value: 'Module' });
|
return Object.defineProperty(n, Symbol.toStringTag, { value: 'Module' });
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = "4.59.1";
|
var version = "4.60.0";
|
||||||
|
|
||||||
// src/vlq.ts
|
// src/vlq.ts
|
||||||
var comma = ",".charCodeAt(0);
|
var comma = ",".charCodeAt(0);
|
||||||
|
|
@ -2464,12 +2464,15 @@ class Variable extends ExpressionEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Synthetic import name for source phase imports, similar to '*' for namespaces */
|
||||||
|
const SOURCE_PHASE_IMPORT = '*source';
|
||||||
class ExternalVariable extends Variable {
|
class ExternalVariable extends Variable {
|
||||||
constructor(module, name) {
|
constructor(module, name) {
|
||||||
super(name);
|
super(name);
|
||||||
this.referenced = false;
|
this.referenced = false;
|
||||||
this.module = module;
|
this.module = module;
|
||||||
this.isNamespace = name === '*';
|
this.isNamespace = name === '*';
|
||||||
|
this.isSourcePhase = name === SOURCE_PHASE_IMPORT;
|
||||||
}
|
}
|
||||||
addReference(identifier) {
|
addReference(identifier) {
|
||||||
this.referenced = true;
|
this.referenced = true;
|
||||||
|
|
@ -8432,6 +8435,13 @@ function getInteropBlock(dependencies, interop, externalLiveBindings, freeze, sy
|
||||||
return `${getHelpersBlock(neededInteropHelpers, accessedGlobals, indent, snippets, externalLiveBindings, freeze, symbols)}${interopStatements.length > 0 ? `${interopStatements.join(n)}${n}${n}` : ''}`;
|
return `${getHelpersBlock(neededInteropHelpers, accessedGlobals, indent, snippets, externalLiveBindings, freeze, symbols)}${interopStatements.length > 0 ? `${interopStatements.join(n)}${n}${n}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throwOnPhase(outputFormat, chunkId, dependencies) {
|
||||||
|
const sourcePhaseDependency = dependencies.find(dependency => dependency.sourcePhaseImport);
|
||||||
|
if (sourcePhaseDependency) {
|
||||||
|
error(logSourcePhaseFormatUnsupported(outputFormat, chunkId, sourcePhaseDependency.importPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addJsExtension(name) {
|
function addJsExtension(name) {
|
||||||
return name.endsWith('.js') ? name : name + '.js';
|
return name.endsWith('.js') ? name : name + '.js';
|
||||||
}
|
}
|
||||||
|
|
@ -8570,6 +8580,7 @@ function warnOnBuiltins(log, dependencies) {
|
||||||
|
|
||||||
function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, log, outro, snippets }, { amd, esModule, externalLiveBindings, freeze, generatedCode: { symbols }, interop, reexportProtoFromExternal, strict }) {
|
function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, log, outro, snippets }, { amd, esModule, externalLiveBindings, freeze, generatedCode: { symbols }, interop, reexportProtoFromExternal, strict }) {
|
||||||
warnOnBuiltins(log, dependencies);
|
warnOnBuiltins(log, dependencies);
|
||||||
|
throwOnPhase('amd', id, dependencies);
|
||||||
const deps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
const deps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
||||||
const parameters = dependencies.map(m => m.name);
|
const parameters = dependencies.map(m => m.name);
|
||||||
const { n, getNonArrowFunctionIntro, _ } = snippets;
|
const { n, getNonArrowFunctionIntro, _ } = snippets;
|
||||||
|
|
@ -8607,7 +8618,8 @@ function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, h
|
||||||
.append(`${n}${n}}));`);
|
.append(`${n}${n}}));`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cjs(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, outro, snippets }, { compact, esModule, externalLiveBindings, freeze, interop, generatedCode: { symbols }, reexportProtoFromExternal, strict }) {
|
function cjs(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, outro, snippets }, { compact, esModule, externalLiveBindings, freeze, interop, generatedCode: { symbols }, reexportProtoFromExternal, strict }) {
|
||||||
|
throwOnPhase('cjs', id, dependencies);
|
||||||
const { _, n } = snippets;
|
const { _, n } = snippets;
|
||||||
const useStrict = strict ? `'use strict';${n}${n}` : '';
|
const useStrict = strict ? `'use strict';${n}${n}` : '';
|
||||||
let namespaceMarkers = getNamespaceMarkers(namedExportsMode && hasExports, isEntryFacade && (esModule === true || (esModule === 'if-default-prop' && hasDefaultExport)), isModuleFacade && symbols, snippets);
|
let namespaceMarkers = getNamespaceMarkers(namedExportsMode && hasExports, isEntryFacade && (esModule === true || (esModule === 'if-default-prop' && hasDefaultExport)), isModuleFacade && symbols, snippets);
|
||||||
|
|
@ -8660,11 +8672,16 @@ function es(magicString, { accessedGlobals, indent: t, intro, outro, dependencie
|
||||||
}
|
}
|
||||||
function getImportBlock(dependencies, importAttributesKey, { _ }) {
|
function getImportBlock(dependencies, importAttributesKey, { _ }) {
|
||||||
const importBlock = [];
|
const importBlock = [];
|
||||||
for (const { importPath, reexports, imports, name, attributes } of dependencies) {
|
for (const { importPath, reexports, imports, name, attributes, sourcePhaseImport } of dependencies) {
|
||||||
const assertion = attributes ? `${_}${importAttributesKey}${_}${attributes}` : '';
|
const assertion = attributes ? `${_}${importAttributesKey}${_}${attributes}` : '';
|
||||||
const pathWithAssertion = `'${importPath}'${assertion};`;
|
const pathWithAssertion = `'${importPath}'${assertion};`;
|
||||||
|
if (sourcePhaseImport) {
|
||||||
|
importBlock.push(`import source ${sourcePhaseImport} from${_}${pathWithAssertion}`);
|
||||||
|
}
|
||||||
if (!reexports && !imports) {
|
if (!reexports && !imports) {
|
||||||
importBlock.push(`import${_}${pathWithAssertion}`);
|
if (!sourcePhaseImport) {
|
||||||
|
importBlock.push(`import${_}${pathWithAssertion}`);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (imports) {
|
if (imports) {
|
||||||
|
|
@ -8818,7 +8835,8 @@ function trimEmptyImports(dependencies) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function iife(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, indent: t, intro, namedExportsMode, log, outro, snippets }, { compact, esModule, extend, freeze, externalLiveBindings, reexportProtoFromExternal, globals, interop, name, generatedCode: { symbols }, strict }) {
|
function iife(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, namedExportsMode, log, outro, snippets }, { compact, esModule, extend, freeze, externalLiveBindings, reexportProtoFromExternal, globals, interop, name, generatedCode: { symbols }, strict }) {
|
||||||
|
throwOnPhase('iife', id, dependencies);
|
||||||
const { _, getNonArrowFunctionIntro, getPropertyAccess, n } = snippets;
|
const { _, getNonArrowFunctionIntro, getPropertyAccess, n } = snippets;
|
||||||
const isNamespaced = name && name.includes('.');
|
const isNamespaced = name && name.includes('.');
|
||||||
const useVariableAssignment = !extend && !isNamespaced;
|
const useVariableAssignment = !extend && !isNamespaced;
|
||||||
|
|
@ -8877,7 +8895,8 @@ function iife(magicString, { accessedGlobals, dependencies, exports: exports$1,
|
||||||
|
|
||||||
const MISSING_EXPORT_SHIM_VARIABLE = '_missingExportShim';
|
const MISSING_EXPORT_SHIM_VARIABLE = '_missingExportShim';
|
||||||
|
|
||||||
function system(magicString, { accessedGlobals, dependencies, exports: exports$1, hasExports, indent: t, intro, snippets, outro, usesTopLevelAwait }, { externalLiveBindings, freeze, name, generatedCode: { symbols }, strict, systemNullSetters }) {
|
function system(magicString, { accessedGlobals, dependencies, exports: exports$1, hasExports, id, indent: t, intro, snippets, outro, usesTopLevelAwait }, { externalLiveBindings, freeze, name, generatedCode: { symbols }, strict, systemNullSetters }) {
|
||||||
|
throwOnPhase('system', id, dependencies);
|
||||||
const { _, getFunctionIntro, getNonArrowFunctionIntro, n, s } = snippets;
|
const { _, getFunctionIntro, getNonArrowFunctionIntro, n, s } = snippets;
|
||||||
const { importBindings, setters, starExcludes } = analyzeDependencies(dependencies, exports$1, t, snippets);
|
const { importBindings, setters, starExcludes } = analyzeDependencies(dependencies, exports$1, t, snippets);
|
||||||
const registeredName = name ? `'${name}',${_}` : '';
|
const registeredName = name ? `'${name}',${_}` : '';
|
||||||
|
|
@ -9045,6 +9064,7 @@ function umd(magicString, { accessedGlobals, dependencies, exports: exports$1, h
|
||||||
if (hasExports && !name) {
|
if (hasExports && !name) {
|
||||||
return error(logMissingNameOptionForUmdExport());
|
return error(logMissingNameOptionForUmdExport());
|
||||||
}
|
}
|
||||||
|
throwOnPhase('umd', id, dependencies);
|
||||||
warnOnBuiltins(log, dependencies);
|
warnOnBuiltins(log, dependencies);
|
||||||
const amdDeps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
const amdDeps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
||||||
const cjsDeps = dependencies.map(m => `require('${m.importPath}')`);
|
const cjsDeps = dependencies.map(m => `require('${m.importPath}')`);
|
||||||
|
|
@ -16120,6 +16140,8 @@ const bufferParsers = [
|
||||||
node.specifiers = convertNodeList(node, scope, buffer[position], buffer);
|
node.specifiers = convertNodeList(node, scope, buffer[position], buffer);
|
||||||
node.source = convertNode(node, scope, buffer[position + 1], buffer);
|
node.source = convertNode(node, scope, buffer[position + 1], buffer);
|
||||||
node.attributes = convertNodeList(node, scope, buffer[position + 2], buffer);
|
node.attributes = convertNodeList(node, scope, buffer[position + 2], buffer);
|
||||||
|
const phaseIndex = buffer[position + 3];
|
||||||
|
node.phase = phaseIndex === 0 ? undefined : FIXED_STRINGS[phaseIndex];
|
||||||
},
|
},
|
||||||
function importDefaultSpecifier(node, position, buffer) {
|
function importDefaultSpecifier(node, position, buffer) {
|
||||||
const { scope } = node;
|
const { scope } = node;
|
||||||
|
|
@ -16131,6 +16153,8 @@ const bufferParsers = [
|
||||||
node.sourceAstNode = convertNode$1(buffer[position], buffer);
|
node.sourceAstNode = convertNode$1(buffer[position], buffer);
|
||||||
const optionsPosition = buffer[position + 1];
|
const optionsPosition = buffer[position + 1];
|
||||||
node.options = optionsPosition === 0 ? null : convertNode(node, scope, optionsPosition, buffer);
|
node.options = optionsPosition === 0 ? null : convertNode(node, scope, optionsPosition, buffer);
|
||||||
|
const phaseIndex = buffer[position + 2];
|
||||||
|
node.phase = phaseIndex === 0 ? undefined : FIXED_STRINGS[phaseIndex];
|
||||||
},
|
},
|
||||||
function importNamespaceSpecifier(node, position, buffer) {
|
function importNamespaceSpecifier(node, position, buffer) {
|
||||||
const { scope } = node;
|
const { scope } = node;
|
||||||
|
|
@ -16916,6 +16940,7 @@ class Module {
|
||||||
this.isUserDefinedEntryPoint = false;
|
this.isUserDefinedEntryPoint = false;
|
||||||
this.needsExportShim = false;
|
this.needsExportShim = false;
|
||||||
this.sideEffectDependenciesByVariable = new Map();
|
this.sideEffectDependenciesByVariable = new Map();
|
||||||
|
this.sourcePhaseSources = new Set();
|
||||||
this.sourcesWithAttributes = new Map();
|
this.sourcesWithAttributes = new Map();
|
||||||
this.allExportsIncluded = false;
|
this.allExportsIncluded = false;
|
||||||
this.ast = null;
|
this.ast = null;
|
||||||
|
|
@ -17566,16 +17591,19 @@ class Module {
|
||||||
if (this.scope.variables.has(localName) || this.importDescriptions.has(localName)) {
|
if (this.scope.variables.has(localName) || this.importDescriptions.has(localName)) {
|
||||||
this.error(logRedeclarationError(localName), specifier.local.start);
|
this.error(logRedeclarationError(localName), specifier.local.start);
|
||||||
}
|
}
|
||||||
const name = specifier instanceof ImportDefaultSpecifier
|
const name = node.phase === 'source'
|
||||||
? 'default'
|
? SOURCE_PHASE_IMPORT
|
||||||
: specifier instanceof ImportNamespaceSpecifier
|
: specifier instanceof ImportDefaultSpecifier
|
||||||
? '*'
|
? 'default'
|
||||||
: specifier.imported instanceof Identifier
|
: specifier instanceof ImportNamespaceSpecifier
|
||||||
? specifier.imported.name
|
? '*'
|
||||||
: specifier.imported.value;
|
: specifier.imported instanceof Identifier
|
||||||
|
? specifier.imported.name
|
||||||
|
: specifier.imported.value;
|
||||||
this.importDescriptions.set(localName, {
|
this.importDescriptions.set(localName, {
|
||||||
module: null, // filled in later
|
module: null, // filled in later
|
||||||
name,
|
name,
|
||||||
|
phase: node.phase === 'source' ? 'source' : 'instance',
|
||||||
source,
|
source,
|
||||||
start: specifier.start
|
start: specifier.start
|
||||||
});
|
});
|
||||||
|
|
@ -17648,6 +17676,9 @@ class Module {
|
||||||
else {
|
else {
|
||||||
this.sourcesWithAttributes.set(source, parsedAttributes);
|
this.sourcesWithAttributes.set(source, parsedAttributes);
|
||||||
}
|
}
|
||||||
|
if (declaration.phase === 'source') {
|
||||||
|
this.sourcePhaseSources.add(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getImportedJsxFactoryVariable(baseName, nodeStart, importSource) {
|
getImportedJsxFactoryVariable(baseName, nodeStart, importSource) {
|
||||||
const { id } = this.resolvedIds[importSource];
|
const { id } = this.resolvedIds[importSource];
|
||||||
|
|
@ -17861,6 +17892,9 @@ function deconflictImportsEsmOrSystem(usedNames, imports, dependenciesToBeDeconf
|
||||||
? externalChunkByModule.get(module)
|
? externalChunkByModule.get(module)
|
||||||
: chunkByModule.get(module)).variableName);
|
: chunkByModule.get(module)).variableName);
|
||||||
}
|
}
|
||||||
|
else if (module instanceof ExternalModule && variable.isSourcePhase) {
|
||||||
|
variable.setRenderNames(null, getSafeName(module.suggestedVariableName + '__source', usedNames, variable.forbiddenNames));
|
||||||
|
}
|
||||||
else if (module instanceof ExternalModule && name === 'default') {
|
else if (module instanceof ExternalModule && name === 'default') {
|
||||||
variable.setRenderNames(null, getSafeName([...module.exportedVariables].some(([exportedVariable, exportedName]) => exportedName === '*' && exportedVariable.included)
|
variable.setRenderNames(null, getSafeName([...module.exportedVariables].some(([exportedVariable, exportedName]) => exportedName === '*' && exportedVariable.included)
|
||||||
? module.suggestedVariableName + '__default'
|
? module.suggestedVariableName + '__default'
|
||||||
|
|
@ -18907,10 +18941,14 @@ class Chunk {
|
||||||
const module = variable.module;
|
const module = variable.module;
|
||||||
let dependency;
|
let dependency;
|
||||||
let imported;
|
let imported;
|
||||||
|
const isSourcePhase = module instanceof ExternalModule && variable.isSourcePhase;
|
||||||
if (module instanceof ExternalModule) {
|
if (module instanceof ExternalModule) {
|
||||||
dependency = this.externalChunkByModule.get(module);
|
dependency = this.externalChunkByModule.get(module);
|
||||||
imported = variable.name;
|
imported = variable.name;
|
||||||
if (imported !== 'default' && imported !== '*' && interop(module.id) === 'defaultOnly') {
|
if (!isSourcePhase &&
|
||||||
|
imported !== 'default' &&
|
||||||
|
imported !== '*' &&
|
||||||
|
interop(module.id) === 'defaultOnly') {
|
||||||
return error(logUnexpectedNamedImport(module.id, imported, false));
|
return error(logUnexpectedNamedImport(module.id, imported, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18920,7 +18958,8 @@ class Chunk {
|
||||||
}
|
}
|
||||||
getOrCreate(importsByDependency, dependency, getNewArray).push({
|
getOrCreate(importsByDependency, dependency, getNewArray).push({
|
||||||
imported,
|
imported,
|
||||||
local: variable.getName(this.snippets.getPropertyAccess)
|
local: variable.getName(this.snippets.getPropertyAccess),
|
||||||
|
phase: isSourcePhase ? 'source' : 'instance'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return importsByDependency;
|
return importsByDependency;
|
||||||
|
|
@ -19071,6 +19110,9 @@ class Chunk {
|
||||||
const reexports = reexportSpecifiers.get(dependency) || null;
|
const reexports = reexportSpecifiers.get(dependency) || null;
|
||||||
const namedExportsMode = dependency instanceof ExternalChunk || dependency.exportMode !== 'default';
|
const namedExportsMode = dependency instanceof ExternalChunk || dependency.exportMode !== 'default';
|
||||||
const importPath = dependency.getImportPath(fileName);
|
const importPath = dependency.getImportPath(fileName);
|
||||||
|
// Separate source-phase imports from regular imports
|
||||||
|
const sourcePhaseImport = imports?.find(index => index.phase === 'source');
|
||||||
|
const instanceImports = imports?.filter(index => index.phase !== 'source') ?? null;
|
||||||
renderedDependencies.set(dependency, {
|
renderedDependencies.set(dependency, {
|
||||||
attributes: dependency instanceof ExternalChunk
|
attributes: dependency instanceof ExternalChunk
|
||||||
? dependency.getImportAttributes(this.snippets)
|
? dependency.getImportAttributes(this.snippets)
|
||||||
|
|
@ -19080,12 +19122,13 @@ class Chunk {
|
||||||
(this.outputOptions.format === 'umd' || this.outputOptions.format === 'iife') &&
|
(this.outputOptions.format === 'umd' || this.outputOptions.format === 'iife') &&
|
||||||
getGlobalName(dependency, this.outputOptions.globals, (imports || reexports) !== null, this.inputOptions.onLog),
|
getGlobalName(dependency, this.outputOptions.globals, (imports || reexports) !== null, this.inputOptions.onLog),
|
||||||
importPath,
|
importPath,
|
||||||
imports,
|
imports: instanceImports && instanceImports.length > 0 ? instanceImports : null,
|
||||||
isChunk: dependency instanceof Chunk,
|
isChunk: dependency instanceof Chunk,
|
||||||
name: dependency.variableName,
|
name: dependency.variableName,
|
||||||
namedExportsMode,
|
namedExportsMode,
|
||||||
namespaceVariableName: dependency.namespaceVariableName,
|
namespaceVariableName: dependency.namespaceVariableName,
|
||||||
reexports
|
reexports,
|
||||||
|
sourcePhaseImport: sourcePhaseImport?.local
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (this.renderedDependencies = renderedDependencies);
|
return (this.renderedDependencies = renderedDependencies);
|
||||||
|
|
@ -21519,13 +21562,16 @@ class ModuleLoader {
|
||||||
return loadNewModulesPromise;
|
return loadNewModulesPromise;
|
||||||
}
|
}
|
||||||
async fetchDynamicDependencies(module, resolveDynamicImportPromises) {
|
async fetchDynamicDependencies(module, resolveDynamicImportPromises) {
|
||||||
const dependencies = await Promise.all(resolveDynamicImportPromises.map(resolveDynamicImportPromise => resolveDynamicImportPromise.then(async ([{ node }, resolvedId]) => {
|
const dependencies = await Promise.all(resolveDynamicImportPromises.map(resolveDynamicImportPromise => resolveDynamicImportPromise.then(async ([{ argument, node }, resolvedId]) => {
|
||||||
if (resolvedId === null)
|
if (resolvedId === null)
|
||||||
return null;
|
return null;
|
||||||
if (typeof resolvedId === 'string') {
|
if (typeof resolvedId === 'string') {
|
||||||
node.resolution = resolvedId;
|
node.resolution = resolvedId;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (node.phase === 'source' && !resolvedId.external) {
|
||||||
|
return error(logNonExternalSourcePhaseImport(typeof argument === 'string' ? argument : relativeId(resolvedId.id), module.id));
|
||||||
|
}
|
||||||
return (node.resolution = await this.fetchResolvedDependency(relativeId(resolvedId.id), module.id, resolvedId));
|
return (node.resolution = await this.fetchResolvedDependency(relativeId(resolvedId.id), module.id, resolvedId));
|
||||||
})));
|
})));
|
||||||
for (const dependency of dependencies) {
|
for (const dependency of dependencies) {
|
||||||
|
|
@ -21605,7 +21651,12 @@ class ModuleLoader {
|
||||||
return this.fetchModule(resolvedId, importer, false, false);
|
return this.fetchModule(resolvedId, importer, false, false);
|
||||||
}
|
}
|
||||||
async fetchStaticDependencies(module, resolveStaticDependencyPromises) {
|
async fetchStaticDependencies(module, resolveStaticDependencyPromises) {
|
||||||
for (const dependency of await Promise.all(resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => resolveStaticDependencyPromise.then(([source, resolvedId]) => this.fetchResolvedDependency(source, module.id, resolvedId))))) {
|
for (const dependency of await Promise.all(resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => resolveStaticDependencyPromise.then(([source, resolvedId]) => {
|
||||||
|
if (module.sourcePhaseSources.has(source) && !resolvedId.external) {
|
||||||
|
return error(logNonExternalSourcePhaseImport(source, module.id));
|
||||||
|
}
|
||||||
|
return this.fetchResolvedDependency(source, module.id, resolvedId);
|
||||||
|
})))) {
|
||||||
module.dependencies.add(dependency);
|
module.dependencies.add(dependency);
|
||||||
dependency.importers.push(module.id);
|
dependency.importers.push(module.id);
|
||||||
}
|
}
|
||||||
|
|
@ -22874,7 +22925,7 @@ class Graph {
|
||||||
warnForMissingExports() {
|
warnForMissingExports() {
|
||||||
for (const module of this.modules) {
|
for (const module of this.modules) {
|
||||||
for (const importDescription of module.importDescriptions.values()) {
|
for (const importDescription of module.importDescriptions.values()) {
|
||||||
if (importDescription.name !== '*') {
|
if (importDescription.name !== '*' && importDescription.phase !== 'source') {
|
||||||
const [variable, options] = importDescription.module.getVariableForExportName(importDescription.name, { importChain: [module.id] });
|
const [variable, options] = importDescription.module.getVariableForExportName(importDescription.name, { importChain: [module.id] });
|
||||||
if (!variable) {
|
if (!variable) {
|
||||||
module.log(LOGLEVEL_WARN, logMissingExport(importDescription.name, module.id, importDescription.module.id, !!options?.missingButExportExists), importDescription.start);
|
module.log(LOGLEVEL_WARN, logMissingExport(importDescription.name, module.id, importDescription.module.id, !!options?.missingButExportExists), importDescription.start);
|
||||||
|
|
|
||||||
36
frontend/node_modules/rollup/dist/es/shared/parseAst.js
generated
vendored
36
frontend/node_modules/rollup/dist/es/shared/parseAst.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
@ -108,7 +108,9 @@ const FIXED_STRINGS = [
|
||||||
'noSideEffects',
|
'noSideEffects',
|
||||||
'sourcemap',
|
'sourcemap',
|
||||||
'using',
|
'using',
|
||||||
'await using'
|
'await using',
|
||||||
|
'source',
|
||||||
|
'defer'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ANNOTATION_KEY = '_rollupAnnotations';
|
const ANNOTATION_KEY = '_rollupAnnotations';
|
||||||
|
|
@ -390,6 +392,8 @@ const URL_TREESHAKE_PURE = 'configuration-options/#pure';
|
||||||
const URL_TREESHAKE_NOSIDEEFFECTS = 'configuration-options/#no-side-effects';
|
const URL_TREESHAKE_NOSIDEEFFECTS = 'configuration-options/#no-side-effects';
|
||||||
const URL_TREESHAKE_MODULESIDEEFFECTS = 'configuration-options/#treeshake-modulesideeffects';
|
const URL_TREESHAKE_MODULESIDEEFFECTS = 'configuration-options/#treeshake-modulesideeffects';
|
||||||
const URL_WATCH = 'configuration-options/#watch';
|
const URL_WATCH = 'configuration-options/#watch';
|
||||||
|
// es-module-syntax
|
||||||
|
const URL_SOURCE_PHASE_IMPORTS = 'es-module-syntax/#source-phase-import';
|
||||||
const URL_GENERATEBUNDLE = 'plugin-development/#generatebundle';
|
const URL_GENERATEBUNDLE = 'plugin-development/#generatebundle';
|
||||||
const URL_LOAD = 'plugin-development/#load';
|
const URL_LOAD = 'plugin-development/#load';
|
||||||
const URL_TRANSFORM = 'plugin-development/#transform';
|
const URL_TRANSFORM = 'plugin-development/#transform';
|
||||||
|
|
@ -447,7 +451,7 @@ function augmentLogMessage(log) {
|
||||||
}
|
}
|
||||||
// Error codes should be sorted alphabetically while errors should be sorted by
|
// Error codes should be sorted alphabetically while errors should be sorted by
|
||||||
// error code below
|
// error code below
|
||||||
const ADDON_ERROR = 'ADDON_ERROR', ALREADY_CLOSED = 'ALREADY_CLOSED', AMBIGUOUS_EXTERNAL_NAMESPACES = 'AMBIGUOUS_EXTERNAL_NAMESPACES', ANONYMOUS_PLUGIN_CACHE = 'ANONYMOUS_PLUGIN_CACHE', ASSET_NOT_FINALISED = 'ASSET_NOT_FINALISED', ASSET_NOT_FOUND = 'ASSET_NOT_FOUND', ASSET_SOURCE_ALREADY_SET = 'ASSET_SOURCE_ALREADY_SET', ASSET_SOURCE_MISSING = 'ASSET_SOURCE_MISSING', BAD_LOADER = 'BAD_LOADER', CANNOT_CALL_NAMESPACE = 'CANNOT_CALL_NAMESPACE', CANNOT_EMIT_FROM_OPTIONS_HOOK = 'CANNOT_EMIT_FROM_OPTIONS_HOOK', CHUNK_NOT_GENERATED = 'CHUNK_NOT_GENERATED', CHUNK_INVALID = 'CHUNK_INVALID', CIRCULAR_CHUNK = 'CIRCULAR_CHUNK', CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY', CIRCULAR_REEXPORT = 'CIRCULAR_REEXPORT', CONST_REASSIGN = 'CONST_REASSIGN', CYCLIC_CROSS_CHUNK_REEXPORT = 'CYCLIC_CROSS_CHUNK_REEXPORT', DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', DUPLICATE_ARGUMENT_NAME = 'DUPLICATE_ARGUMENT_NAME', DUPLICATE_EXPORT = 'DUPLICATE_EXPORT', DUPLICATE_PLUGIN_NAME = 'DUPLICATE_PLUGIN_NAME', EMPTY_BUNDLE = 'EMPTY_BUNDLE', EVAL = 'EVAL', EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS = 'EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS', EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES = 'EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', INCONSISTENT_IMPORT_ATTRIBUTES = 'INCONSISTENT_IMPORT_ATTRIBUTES', INVALID_ANNOTATION = 'INVALID_ANNOTATION', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_EXPORT_OPTION = 'INVALID_EXPORT_OPTION', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_IMPORT_ATTRIBUTE = 'INVALID_IMPORT_ATTRIBUTE', INVALID_LOG_POSITION = 'INVALID_LOG_POSITION', INVALID_OPTION = 'INVALID_OPTION', INVALID_PLUGIN_HOOK = 'INVALID_PLUGIN_HOOK', INVALID_ROLLUP_PHASE = 'INVALID_ROLLUP_PHASE', INVALID_SETASSETSOURCE = 'INVALID_SETASSETSOURCE', INVALID_TLA_FORMAT = 'INVALID_TLA_FORMAT', MISSING_EXPORT = 'MISSING_EXPORT', MISSING_GLOBAL_NAME = 'MISSING_GLOBAL_NAME', MISSING_IMPLICIT_DEPENDANT = 'MISSING_IMPLICIT_DEPENDANT', MISSING_JSX_EXPORT = 'MISSING_JSX_EXPORT', MISSING_NAME_OPTION_FOR_IIFE_EXPORT = 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT', MISSING_NODE_BUILTINS = 'MISSING_NODE_BUILTINS', MISSING_OPTION = 'MISSING_OPTION', MIXED_EXPORTS = 'MIXED_EXPORTS', MODULE_LEVEL_DIRECTIVE = 'MODULE_LEVEL_DIRECTIVE', NAMESPACE_CONFLICT = 'NAMESPACE_CONFLICT', NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE = 'NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE', OPTIMIZE_CHUNK_STATUS = 'OPTIMIZE_CHUNK_STATUS', PARSE_ERROR = 'PARSE_ERROR', PLUGIN_ERROR = 'PLUGIN_ERROR', REDECLARATION_ERROR = 'REDECLARATION_ERROR', RESERVED_NAMESPACE = 'RESERVED_NAMESPACE', SHIMMED_EXPORT = 'SHIMMED_EXPORT', SOURCEMAP_BROKEN = 'SOURCEMAP_BROKEN', SOURCEMAP_ERROR = 'SOURCEMAP_ERROR', SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT', THIS_IS_UNDEFINED = 'THIS_IS_UNDEFINED', UNEXPECTED_NAMED_IMPORT = 'UNEXPECTED_NAMED_IMPORT', UNKNOWN_OPTION = 'UNKNOWN_OPTION', UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY', UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT', UNUSED_EXTERNAL_IMPORT = 'UNUSED_EXTERNAL_IMPORT', VALIDATION_ERROR = 'VALIDATION_ERROR';
|
const ADDON_ERROR = 'ADDON_ERROR', ALREADY_CLOSED = 'ALREADY_CLOSED', AMBIGUOUS_EXTERNAL_NAMESPACES = 'AMBIGUOUS_EXTERNAL_NAMESPACES', ANONYMOUS_PLUGIN_CACHE = 'ANONYMOUS_PLUGIN_CACHE', ASSET_NOT_FINALISED = 'ASSET_NOT_FINALISED', ASSET_NOT_FOUND = 'ASSET_NOT_FOUND', ASSET_SOURCE_ALREADY_SET = 'ASSET_SOURCE_ALREADY_SET', ASSET_SOURCE_MISSING = 'ASSET_SOURCE_MISSING', BAD_LOADER = 'BAD_LOADER', CANNOT_CALL_NAMESPACE = 'CANNOT_CALL_NAMESPACE', CANNOT_EMIT_FROM_OPTIONS_HOOK = 'CANNOT_EMIT_FROM_OPTIONS_HOOK', CHUNK_NOT_GENERATED = 'CHUNK_NOT_GENERATED', CHUNK_INVALID = 'CHUNK_INVALID', CIRCULAR_CHUNK = 'CIRCULAR_CHUNK', CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY', CIRCULAR_REEXPORT = 'CIRCULAR_REEXPORT', CONST_REASSIGN = 'CONST_REASSIGN', CYCLIC_CROSS_CHUNK_REEXPORT = 'CYCLIC_CROSS_CHUNK_REEXPORT', DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', DUPLICATE_ARGUMENT_NAME = 'DUPLICATE_ARGUMENT_NAME', DUPLICATE_EXPORT = 'DUPLICATE_EXPORT', DUPLICATE_PLUGIN_NAME = 'DUPLICATE_PLUGIN_NAME', EMPTY_BUNDLE = 'EMPTY_BUNDLE', EVAL = 'EVAL', EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS = 'EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS', EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES = 'EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', INCONSISTENT_IMPORT_ATTRIBUTES = 'INCONSISTENT_IMPORT_ATTRIBUTES', INVALID_ANNOTATION = 'INVALID_ANNOTATION', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_EXPORT_OPTION = 'INVALID_EXPORT_OPTION', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_IMPORT_ATTRIBUTE = 'INVALID_IMPORT_ATTRIBUTE', INVALID_LOG_POSITION = 'INVALID_LOG_POSITION', INVALID_OPTION = 'INVALID_OPTION', INVALID_PLUGIN_HOOK = 'INVALID_PLUGIN_HOOK', INVALID_ROLLUP_PHASE = 'INVALID_ROLLUP_PHASE', INVALID_SETASSETSOURCE = 'INVALID_SETASSETSOURCE', INVALID_TLA_FORMAT = 'INVALID_TLA_FORMAT', MISSING_EXPORT = 'MISSING_EXPORT', MISSING_GLOBAL_NAME = 'MISSING_GLOBAL_NAME', MISSING_IMPLICIT_DEPENDANT = 'MISSING_IMPLICIT_DEPENDANT', MISSING_JSX_EXPORT = 'MISSING_JSX_EXPORT', MISSING_NAME_OPTION_FOR_IIFE_EXPORT = 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT', MISSING_NODE_BUILTINS = 'MISSING_NODE_BUILTINS', MISSING_OPTION = 'MISSING_OPTION', MIXED_EXPORTS = 'MIXED_EXPORTS', MODULE_LEVEL_DIRECTIVE = 'MODULE_LEVEL_DIRECTIVE', NAMESPACE_CONFLICT = 'NAMESPACE_CONFLICT', NON_EXTERNAL_SOURCE_PHASE_IMPORT = 'NON_EXTERNAL_SOURCE_PHASE_IMPORT', NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE = 'NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE', OPTIMIZE_CHUNK_STATUS = 'OPTIMIZE_CHUNK_STATUS', PARSE_ERROR = 'PARSE_ERROR', PLUGIN_ERROR = 'PLUGIN_ERROR', REDECLARATION_ERROR = 'REDECLARATION_ERROR', RESERVED_NAMESPACE = 'RESERVED_NAMESPACE', SHIMMED_EXPORT = 'SHIMMED_EXPORT', SOURCE_PHASE_FORMAT_UNSUPPORTED = 'SOURCE_PHASE_FORMAT_UNSUPPORTED', SOURCEMAP_BROKEN = 'SOURCEMAP_BROKEN', SOURCEMAP_ERROR = 'SOURCEMAP_ERROR', SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT', THIS_IS_UNDEFINED = 'THIS_IS_UNDEFINED', UNEXPECTED_NAMED_IMPORT = 'UNEXPECTED_NAMED_IMPORT', UNKNOWN_OPTION = 'UNKNOWN_OPTION', UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY', UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT', UNUSED_EXTERNAL_IMPORT = 'UNUSED_EXTERNAL_IMPORT', VALIDATION_ERROR = 'VALIDATION_ERROR';
|
||||||
function logAddonNotGenerated(message, hook, plugin) {
|
function logAddonNotGenerated(message, hook, plugin) {
|
||||||
return {
|
return {
|
||||||
code: ADDON_ERROR,
|
code: ADDON_ERROR,
|
||||||
|
|
@ -892,6 +896,13 @@ function logNamespaceConflict(binding, reexportingModuleId, sources) {
|
||||||
reexporter: reexportingModuleId
|
reexporter: reexportingModuleId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function logNonExternalSourcePhaseImport(source, importer) {
|
||||||
|
return {
|
||||||
|
code: NON_EXTERNAL_SOURCE_PHASE_IMPORT,
|
||||||
|
message: `Source phase import "${source}" in "${relativeId(importer)}" must be external. Source phase imports are only supported for external modules. Use the "external" option to mark this module as external.`,
|
||||||
|
url: getRollupUrl(URL_SOURCE_PHASE_IMPORTS)
|
||||||
|
};
|
||||||
|
}
|
||||||
function logNoTransformMapOrAstWithoutCode(pluginName) {
|
function logNoTransformMapOrAstWithoutCode(pluginName) {
|
||||||
return {
|
return {
|
||||||
code: NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE,
|
code: NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE,
|
||||||
|
|
@ -963,6 +974,13 @@ function logShimmedExport(id, binding) {
|
||||||
message: `Missing export "${binding}" has been shimmed in module "${relativeId(id)}".`
|
message: `Missing export "${binding}" has been shimmed in module "${relativeId(id)}".`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function logSourcePhaseFormatUnsupported(outputFormat, chunkId, dependencyId) {
|
||||||
|
return {
|
||||||
|
code: SOURCE_PHASE_FORMAT_UNSUPPORTED,
|
||||||
|
message: `Source phase imports are not supported for the "${outputFormat}" output format, importing "${dependencyId}" in "${chunkId}". Use the "es" output format to support source phase imports.`,
|
||||||
|
url: getRollupUrl(URL_SOURCE_PHASE_IMPORTS)
|
||||||
|
};
|
||||||
|
}
|
||||||
function logSourcemapBroken(plugin) {
|
function logSourcemapBroken(plugin) {
|
||||||
return {
|
return {
|
||||||
code: SOURCEMAP_BROKEN,
|
code: SOURCEMAP_BROKEN,
|
||||||
|
|
@ -1496,13 +1514,15 @@ const nodeConverters = [
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importDeclaration(position, buffer) {
|
function importDeclaration(position, buffer) {
|
||||||
|
const phaseIndex = buffer[position + 5];
|
||||||
return {
|
return {
|
||||||
type: 'ImportDeclaration',
|
type: 'ImportDeclaration',
|
||||||
start: buffer[position],
|
start: buffer[position],
|
||||||
end: buffer[position + 1],
|
end: buffer[position + 1],
|
||||||
specifiers: convertNodeList(buffer[position + 2], buffer),
|
specifiers: convertNodeList(buffer[position + 2], buffer),
|
||||||
source: convertNode(buffer[position + 3], buffer),
|
source: convertNode(buffer[position + 3], buffer),
|
||||||
attributes: convertNodeList(buffer[position + 4], buffer)
|
attributes: convertNodeList(buffer[position + 4], buffer),
|
||||||
|
...(phaseIndex === 0 ? {} : { phase: FIXED_STRINGS[phaseIndex] })
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importDefaultSpecifier(position, buffer) {
|
function importDefaultSpecifier(position, buffer) {
|
||||||
|
|
@ -1515,12 +1535,14 @@ const nodeConverters = [
|
||||||
},
|
},
|
||||||
function importExpression(position, buffer) {
|
function importExpression(position, buffer) {
|
||||||
const optionsPosition = buffer[position + 3];
|
const optionsPosition = buffer[position + 3];
|
||||||
|
const phaseIndex = buffer[position + 4];
|
||||||
return {
|
return {
|
||||||
type: 'ImportExpression',
|
type: 'ImportExpression',
|
||||||
start: buffer[position],
|
start: buffer[position],
|
||||||
end: buffer[position + 1],
|
end: buffer[position + 1],
|
||||||
source: convertNode(buffer[position + 2], buffer),
|
source: convertNode(buffer[position + 2], buffer),
|
||||||
options: optionsPosition === 0 ? null : convertNode(optionsPosition, buffer)
|
options: optionsPosition === 0 ? null : convertNode(optionsPosition, buffer),
|
||||||
|
...(phaseIndex === 0 ? {} : { phase: FIXED_STRINGS[phaseIndex] })
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importNamespaceSpecifier(position, buffer) {
|
function importNamespaceSpecifier(position, buffer) {
|
||||||
|
|
@ -2099,4 +2121,4 @@ function getAstBuffer(astBuffer) {
|
||||||
const parseAst = (input, { allowReturnOutsideFunction = false, jsx = false } = {}) => convertProgram(getAstBuffer(parse(input, allowReturnOutsideFunction, jsx)));
|
const parseAst = (input, { allowReturnOutsideFunction = false, jsx = false } = {}) => convertProgram(getAstBuffer(parse(input, allowReturnOutsideFunction, jsx)));
|
||||||
const parseAstAsync = async (input, { allowReturnOutsideFunction = false, jsx = false, signal } = {}) => convertProgram(getAstBuffer(await parseAsync(input, allowReturnOutsideFunction, jsx, signal)));
|
const parseAstAsync = async (input, { allowReturnOutsideFunction = false, jsx = false, signal } = {}) => convertProgram(getAstBuffer(await parseAsync(input, allowReturnOutsideFunction, jsx, signal)));
|
||||||
|
|
||||||
export { ANNOTATION_KEY, ArrowFunctionExpression, AwaitExpression, BLANK, BlockStatement, CallExpression, CatchClause, EMPTY_ARRAY, EMPTY_OBJECT, EMPTY_SET, ExportDefaultDeclaration, ExpressionStatement, FIXED_STRINGS, FunctionExpression, INVALID_ANNOTATION_KEY, Identifier, LOGLEVEL_DEBUG, LOGLEVEL_ERROR, LOGLEVEL_INFO, LOGLEVEL_WARN, Literal, MemberExpression, ObjectExpression, Program, Property, RestElement, ReturnStatement, StaticBlock, TemplateLiteral, URL_GENERATEBUNDLE, URL_JSX, URL_LOAD, URL_OUTPUT_AMD_BASEPATH, URL_OUTPUT_AMD_ID, URL_OUTPUT_DIR, URL_OUTPUT_EXTERNALIMPORTATTRIBUTES, URL_OUTPUT_FORMAT, URL_OUTPUT_GENERATEDCODE, URL_OUTPUT_INLINEDYNAMICIMPORTS, URL_OUTPUT_INTEROP, URL_OUTPUT_MANUALCHUNKS, URL_OUTPUT_SOURCEMAPBASEURL, URL_OUTPUT_SOURCEMAPFILE, URL_PRESERVEENTRYSIGNATURES, URL_TRANSFORM, URL_TREESHAKE, URL_TREESHAKE_MODULESIDEEFFECTS, URL_WATCH, VariableDeclarator, addTrailingSlashIfMissed, augmentCodeLocation, augmentLogMessage, convertAnnotations, convertNode, error, getAliasName, getAstBuffer, getImportPath, getRollupError, isAbsolute, isPathFragment, isRelative, isValidUrl, locate, logAddonNotGenerated, logAlreadyClosed, logAmbiguousExternalNamespaces, logAnonymousPluginCache, logAssetNotFinalisedForFileName, logAssetReferenceIdNotFoundForSetSource, logAssetSourceAlreadySet, logBadLoader, logCannotAssignModuleToChunk, logCannotCallNamespace, logCannotEmitFromOptionsHook, logChunkInvalid, logChunkNotGeneratedForFileName, logCircularChunk, logCircularDependency, logCircularReexport, logConflictingSourcemapSources, logConstVariableReassignError, logCyclicCrossChunkReexport, logDuplicateArgumentNameError, logDuplicateExportError, logDuplicatePluginName, logEmptyChunk, logEntryCannotBeExternal, logEval, logExternalModulesCannotBeIncludedInManualChunks, logExternalModulesCannotBeTransformedToModules, logExternalSyntheticExports, logFailedValidation, logFileNameConflict, logFileNameOutsideOutputDirectory, logFileReferenceIdNotFoundForFilename, logFirstSideEffect, logIllegalIdentifierAsName, logIllegalImportReassignment, logImplicitDependantCannotBeExternal, logImplicitDependantIsNotIncluded, logImportAttributeIsInvalid, logImportOptionsAreInvalid, logIncompatibleExportOptionValue, logInconsistentImportAttributes, logInputHookInOutputPlugin, logInternalIdCannotBeExternal, logInvalidAddonPluginHook, logInvalidAnnotation, logInvalidExportOptionValue, logInvalidFormatForTopLevelAwait, logInvalidFunctionPluginHook, logInvalidLogPosition, logInvalidOption, logInvalidRollupPhaseForChunkEmission, logInvalidSetAssetSourceCall, logInvalidSourcemapForError, logLevelPriority, logMissingEntryExport, logMissingExport, logMissingFileOrDirOption, logMissingGlobalName, logMissingJsxExport, logMissingNameOptionForIifeExport, logMissingNameOptionForUmdExport, logMissingNodeBuiltins, logMixedExport, logModuleLevelDirective, logModuleParseError, logNamespaceConflict, logNoAssetSourceSet, logNoTransformMapOrAstWithoutCode, logOptimizeChunkStatus, logParseError, logPluginError, logRedeclarationError, logReservedNamespace, logShimmedExport, logSourcemapBroken, logSyntheticNamedExportsNeedNamespaceExport, logThisIsUndefined, logUnexpectedNamedImport, logUnexpectedNamespaceReexport, logUnknownOption, logUnresolvedEntry, logUnresolvedImplicitDependant, logUnresolvedImport, logUnresolvedImportTreatedAsExternal, logUnusedExternalImports, normalize, parseAst, parseAstAsync, printQuotedStringList, relative, relativeId, warnDeprecation };
|
export { ANNOTATION_KEY, ArrowFunctionExpression, AwaitExpression, BLANK, BlockStatement, CallExpression, CatchClause, EMPTY_ARRAY, EMPTY_OBJECT, EMPTY_SET, ExportDefaultDeclaration, ExpressionStatement, FIXED_STRINGS, FunctionExpression, INVALID_ANNOTATION_KEY, Identifier, LOGLEVEL_DEBUG, LOGLEVEL_ERROR, LOGLEVEL_INFO, LOGLEVEL_WARN, Literal, MemberExpression, ObjectExpression, Program, Property, RestElement, ReturnStatement, StaticBlock, TemplateLiteral, URL_GENERATEBUNDLE, URL_JSX, URL_LOAD, URL_OUTPUT_AMD_BASEPATH, URL_OUTPUT_AMD_ID, URL_OUTPUT_DIR, URL_OUTPUT_EXTERNALIMPORTATTRIBUTES, URL_OUTPUT_FORMAT, URL_OUTPUT_GENERATEDCODE, URL_OUTPUT_INLINEDYNAMICIMPORTS, URL_OUTPUT_INTEROP, URL_OUTPUT_MANUALCHUNKS, URL_OUTPUT_SOURCEMAPBASEURL, URL_OUTPUT_SOURCEMAPFILE, URL_PRESERVEENTRYSIGNATURES, URL_TRANSFORM, URL_TREESHAKE, URL_TREESHAKE_MODULESIDEEFFECTS, URL_WATCH, VariableDeclarator, addTrailingSlashIfMissed, augmentCodeLocation, augmentLogMessage, convertAnnotations, convertNode, error, getAliasName, getAstBuffer, getImportPath, getRollupError, isAbsolute, isPathFragment, isRelative, isValidUrl, locate, logAddonNotGenerated, logAlreadyClosed, logAmbiguousExternalNamespaces, logAnonymousPluginCache, logAssetNotFinalisedForFileName, logAssetReferenceIdNotFoundForSetSource, logAssetSourceAlreadySet, logBadLoader, logCannotAssignModuleToChunk, logCannotCallNamespace, logCannotEmitFromOptionsHook, logChunkInvalid, logChunkNotGeneratedForFileName, logCircularChunk, logCircularDependency, logCircularReexport, logConflictingSourcemapSources, logConstVariableReassignError, logCyclicCrossChunkReexport, logDuplicateArgumentNameError, logDuplicateExportError, logDuplicatePluginName, logEmptyChunk, logEntryCannotBeExternal, logEval, logExternalModulesCannotBeIncludedInManualChunks, logExternalModulesCannotBeTransformedToModules, logExternalSyntheticExports, logFailedValidation, logFileNameConflict, logFileNameOutsideOutputDirectory, logFileReferenceIdNotFoundForFilename, logFirstSideEffect, logIllegalIdentifierAsName, logIllegalImportReassignment, logImplicitDependantCannotBeExternal, logImplicitDependantIsNotIncluded, logImportAttributeIsInvalid, logImportOptionsAreInvalid, logIncompatibleExportOptionValue, logInconsistentImportAttributes, logInputHookInOutputPlugin, logInternalIdCannotBeExternal, logInvalidAddonPluginHook, logInvalidAnnotation, logInvalidExportOptionValue, logInvalidFormatForTopLevelAwait, logInvalidFunctionPluginHook, logInvalidLogPosition, logInvalidOption, logInvalidRollupPhaseForChunkEmission, logInvalidSetAssetSourceCall, logInvalidSourcemapForError, logLevelPriority, logMissingEntryExport, logMissingExport, logMissingFileOrDirOption, logMissingGlobalName, logMissingJsxExport, logMissingNameOptionForIifeExport, logMissingNameOptionForUmdExport, logMissingNodeBuiltins, logMixedExport, logModuleLevelDirective, logModuleParseError, logNamespaceConflict, logNoAssetSourceSet, logNoTransformMapOrAstWithoutCode, logNonExternalSourcePhaseImport, logOptimizeChunkStatus, logParseError, logPluginError, logRedeclarationError, logReservedNamespace, logShimmedExport, logSourcePhaseFormatUnsupported, logSourcemapBroken, logSyntheticNamedExportsNeedNamespaceExport, logThisIsUndefined, logUnexpectedNamedImport, logUnexpectedNamespaceReexport, logUnknownOption, logUnresolvedEntry, logUnresolvedImplicitDependant, logUnresolvedImport, logUnresolvedImportTreatedAsExternal, logUnusedExternalImports, normalize, parseAst, parseAstAsync, printQuotedStringList, relative, relativeId, warnDeprecation };
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/es/shared/watch.js
generated
vendored
4
frontend/node_modules/rollup/dist/es/shared/watch.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/getLogFilter.js
generated
vendored
4
frontend/node_modules/rollup/dist/getLogFilter.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/loadConfigFile.js
generated
vendored
4
frontend/node_modules/rollup/dist/loadConfigFile.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/parseAst.js
generated
vendored
4
frontend/node_modules/rollup/dist/parseAst.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/rollup.js
generated
vendored
4
frontend/node_modules/rollup/dist/rollup.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/shared/fsevents-importer.js
generated
vendored
4
frontend/node_modules/rollup/dist/shared/fsevents-importer.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/shared/index.js
generated
vendored
4
frontend/node_modules/rollup/dist/shared/index.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/shared/loadConfigFile.js
generated
vendored
4
frontend/node_modules/rollup/dist/shared/loadConfigFile.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
36
frontend/node_modules/rollup/dist/shared/parseAst.js
generated
vendored
36
frontend/node_modules/rollup/dist/shared/parseAst.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
@ -272,6 +272,8 @@ const URL_TREESHAKE_PURE = 'configuration-options/#pure';
|
||||||
const URL_TREESHAKE_NOSIDEEFFECTS = 'configuration-options/#no-side-effects';
|
const URL_TREESHAKE_NOSIDEEFFECTS = 'configuration-options/#no-side-effects';
|
||||||
const URL_TREESHAKE_MODULESIDEEFFECTS = 'configuration-options/#treeshake-modulesideeffects';
|
const URL_TREESHAKE_MODULESIDEEFFECTS = 'configuration-options/#treeshake-modulesideeffects';
|
||||||
const URL_WATCH = 'configuration-options/#watch';
|
const URL_WATCH = 'configuration-options/#watch';
|
||||||
|
// es-module-syntax
|
||||||
|
const URL_SOURCE_PHASE_IMPORTS = 'es-module-syntax/#source-phase-import';
|
||||||
// command-line-interface
|
// command-line-interface
|
||||||
const URL_BUNDLE_CONFIG_AS_CJS = 'command-line-interface/#bundleconfigascjs';
|
const URL_BUNDLE_CONFIG_AS_CJS = 'command-line-interface/#bundleconfigascjs';
|
||||||
const URL_CONFIGURATION_FILES = 'command-line-interface/#configuration-files';
|
const URL_CONFIGURATION_FILES = 'command-line-interface/#configuration-files';
|
||||||
|
|
@ -332,7 +334,7 @@ function augmentLogMessage(log) {
|
||||||
}
|
}
|
||||||
// Error codes should be sorted alphabetically while errors should be sorted by
|
// Error codes should be sorted alphabetically while errors should be sorted by
|
||||||
// error code below
|
// error code below
|
||||||
const ADDON_ERROR = 'ADDON_ERROR', ALREADY_CLOSED = 'ALREADY_CLOSED', AMBIGUOUS_EXTERNAL_NAMESPACES = 'AMBIGUOUS_EXTERNAL_NAMESPACES', ANONYMOUS_PLUGIN_CACHE = 'ANONYMOUS_PLUGIN_CACHE', ASSET_NOT_FINALISED = 'ASSET_NOT_FINALISED', ASSET_NOT_FOUND = 'ASSET_NOT_FOUND', ASSET_SOURCE_ALREADY_SET = 'ASSET_SOURCE_ALREADY_SET', ASSET_SOURCE_MISSING = 'ASSET_SOURCE_MISSING', BAD_LOADER = 'BAD_LOADER', CANNOT_CALL_NAMESPACE = 'CANNOT_CALL_NAMESPACE', CANNOT_EMIT_FROM_OPTIONS_HOOK = 'CANNOT_EMIT_FROM_OPTIONS_HOOK', CHUNK_NOT_GENERATED = 'CHUNK_NOT_GENERATED', CHUNK_INVALID = 'CHUNK_INVALID', CIRCULAR_CHUNK = 'CIRCULAR_CHUNK', CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY', CIRCULAR_REEXPORT = 'CIRCULAR_REEXPORT', CONST_REASSIGN = 'CONST_REASSIGN', CYCLIC_CROSS_CHUNK_REEXPORT = 'CYCLIC_CROSS_CHUNK_REEXPORT', DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', DUPLICATE_ARGUMENT_NAME = 'DUPLICATE_ARGUMENT_NAME', DUPLICATE_EXPORT = 'DUPLICATE_EXPORT', DUPLICATE_IMPORT_OPTIONS = 'DUPLICATE_IMPORT_OPTIONS', DUPLICATE_PLUGIN_NAME = 'DUPLICATE_PLUGIN_NAME', EMPTY_BUNDLE = 'EMPTY_BUNDLE', EVAL = 'EVAL', EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS = 'EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS', EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES = 'EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FAIL_AFTER_WARNINGS = 'FAIL_AFTER_WARNINGS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', INCONSISTENT_IMPORT_ATTRIBUTES = 'INCONSISTENT_IMPORT_ATTRIBUTES', INVALID_ANNOTATION = 'INVALID_ANNOTATION', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_CONFIG_MODULE_FORMAT = 'INVALID_CONFIG_MODULE_FORMAT', INVALID_EXPORT_OPTION = 'INVALID_EXPORT_OPTION', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_IMPORT_ATTRIBUTE = 'INVALID_IMPORT_ATTRIBUTE', INVALID_LOG_POSITION = 'INVALID_LOG_POSITION', INVALID_OPTION = 'INVALID_OPTION', INVALID_PLUGIN_HOOK = 'INVALID_PLUGIN_HOOK', INVALID_ROLLUP_PHASE = 'INVALID_ROLLUP_PHASE', INVALID_SETASSETSOURCE = 'INVALID_SETASSETSOURCE', INVALID_TLA_FORMAT = 'INVALID_TLA_FORMAT', MISSING_CONFIG = 'MISSING_CONFIG', MISSING_EXPORT = 'MISSING_EXPORT', MISSING_EXTERNAL_CONFIG = 'MISSING_EXTERNAL_CONFIG', MISSING_GLOBAL_NAME = 'MISSING_GLOBAL_NAME', MISSING_IMPLICIT_DEPENDANT = 'MISSING_IMPLICIT_DEPENDANT', MISSING_JSX_EXPORT = 'MISSING_JSX_EXPORT', MISSING_NAME_OPTION_FOR_IIFE_EXPORT = 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT', MISSING_NODE_BUILTINS = 'MISSING_NODE_BUILTINS', MISSING_OPTION = 'MISSING_OPTION', MIXED_EXPORTS = 'MIXED_EXPORTS', MODULE_LEVEL_DIRECTIVE = 'MODULE_LEVEL_DIRECTIVE', NAMESPACE_CONFLICT = 'NAMESPACE_CONFLICT', NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE = 'NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE', ONLY_INLINE_SOURCEMAPS = 'ONLY_INLINE_SOURCEMAPS', OPTIMIZE_CHUNK_STATUS = 'OPTIMIZE_CHUNK_STATUS', PARSE_ERROR = 'PARSE_ERROR', PLUGIN_ERROR = 'PLUGIN_ERROR', REDECLARATION_ERROR = 'REDECLARATION_ERROR', RESERVED_NAMESPACE = 'RESERVED_NAMESPACE', SHIMMED_EXPORT = 'SHIMMED_EXPORT', SOURCEMAP_BROKEN = 'SOURCEMAP_BROKEN', SOURCEMAP_ERROR = 'SOURCEMAP_ERROR', SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT', THIS_IS_UNDEFINED = 'THIS_IS_UNDEFINED', UNEXPECTED_NAMED_IMPORT = 'UNEXPECTED_NAMED_IMPORT', UNKNOWN_OPTION = 'UNKNOWN_OPTION', UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY', UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT', UNUSED_EXTERNAL_IMPORT = 'UNUSED_EXTERNAL_IMPORT', VALIDATION_ERROR = 'VALIDATION_ERROR';
|
const ADDON_ERROR = 'ADDON_ERROR', ALREADY_CLOSED = 'ALREADY_CLOSED', AMBIGUOUS_EXTERNAL_NAMESPACES = 'AMBIGUOUS_EXTERNAL_NAMESPACES', ANONYMOUS_PLUGIN_CACHE = 'ANONYMOUS_PLUGIN_CACHE', ASSET_NOT_FINALISED = 'ASSET_NOT_FINALISED', ASSET_NOT_FOUND = 'ASSET_NOT_FOUND', ASSET_SOURCE_ALREADY_SET = 'ASSET_SOURCE_ALREADY_SET', ASSET_SOURCE_MISSING = 'ASSET_SOURCE_MISSING', BAD_LOADER = 'BAD_LOADER', CANNOT_CALL_NAMESPACE = 'CANNOT_CALL_NAMESPACE', CANNOT_EMIT_FROM_OPTIONS_HOOK = 'CANNOT_EMIT_FROM_OPTIONS_HOOK', CHUNK_NOT_GENERATED = 'CHUNK_NOT_GENERATED', CHUNK_INVALID = 'CHUNK_INVALID', CIRCULAR_CHUNK = 'CIRCULAR_CHUNK', CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY', CIRCULAR_REEXPORT = 'CIRCULAR_REEXPORT', CONST_REASSIGN = 'CONST_REASSIGN', CYCLIC_CROSS_CHUNK_REEXPORT = 'CYCLIC_CROSS_CHUNK_REEXPORT', DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', DUPLICATE_ARGUMENT_NAME = 'DUPLICATE_ARGUMENT_NAME', DUPLICATE_EXPORT = 'DUPLICATE_EXPORT', DUPLICATE_IMPORT_OPTIONS = 'DUPLICATE_IMPORT_OPTIONS', DUPLICATE_PLUGIN_NAME = 'DUPLICATE_PLUGIN_NAME', EMPTY_BUNDLE = 'EMPTY_BUNDLE', EVAL = 'EVAL', EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS = 'EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS', EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES = 'EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FAIL_AFTER_WARNINGS = 'FAIL_AFTER_WARNINGS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', ILLEGAL_REASSIGNMENT = 'ILLEGAL_REASSIGNMENT', INCONSISTENT_IMPORT_ATTRIBUTES = 'INCONSISTENT_IMPORT_ATTRIBUTES', INVALID_ANNOTATION = 'INVALID_ANNOTATION', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_CONFIG_MODULE_FORMAT = 'INVALID_CONFIG_MODULE_FORMAT', INVALID_EXPORT_OPTION = 'INVALID_EXPORT_OPTION', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_IMPORT_ATTRIBUTE = 'INVALID_IMPORT_ATTRIBUTE', INVALID_LOG_POSITION = 'INVALID_LOG_POSITION', INVALID_OPTION = 'INVALID_OPTION', INVALID_PLUGIN_HOOK = 'INVALID_PLUGIN_HOOK', INVALID_ROLLUP_PHASE = 'INVALID_ROLLUP_PHASE', INVALID_SETASSETSOURCE = 'INVALID_SETASSETSOURCE', INVALID_TLA_FORMAT = 'INVALID_TLA_FORMAT', MISSING_CONFIG = 'MISSING_CONFIG', MISSING_EXPORT = 'MISSING_EXPORT', MISSING_EXTERNAL_CONFIG = 'MISSING_EXTERNAL_CONFIG', MISSING_GLOBAL_NAME = 'MISSING_GLOBAL_NAME', MISSING_IMPLICIT_DEPENDANT = 'MISSING_IMPLICIT_DEPENDANT', MISSING_JSX_EXPORT = 'MISSING_JSX_EXPORT', MISSING_NAME_OPTION_FOR_IIFE_EXPORT = 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT', MISSING_NODE_BUILTINS = 'MISSING_NODE_BUILTINS', MISSING_OPTION = 'MISSING_OPTION', MIXED_EXPORTS = 'MIXED_EXPORTS', MODULE_LEVEL_DIRECTIVE = 'MODULE_LEVEL_DIRECTIVE', NAMESPACE_CONFLICT = 'NAMESPACE_CONFLICT', NON_EXTERNAL_SOURCE_PHASE_IMPORT = 'NON_EXTERNAL_SOURCE_PHASE_IMPORT', NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE = 'NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE', ONLY_INLINE_SOURCEMAPS = 'ONLY_INLINE_SOURCEMAPS', OPTIMIZE_CHUNK_STATUS = 'OPTIMIZE_CHUNK_STATUS', PARSE_ERROR = 'PARSE_ERROR', PLUGIN_ERROR = 'PLUGIN_ERROR', REDECLARATION_ERROR = 'REDECLARATION_ERROR', RESERVED_NAMESPACE = 'RESERVED_NAMESPACE', SHIMMED_EXPORT = 'SHIMMED_EXPORT', SOURCE_PHASE_FORMAT_UNSUPPORTED = 'SOURCE_PHASE_FORMAT_UNSUPPORTED', SOURCEMAP_BROKEN = 'SOURCEMAP_BROKEN', SOURCEMAP_ERROR = 'SOURCEMAP_ERROR', SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT', THIS_IS_UNDEFINED = 'THIS_IS_UNDEFINED', UNEXPECTED_NAMED_IMPORT = 'UNEXPECTED_NAMED_IMPORT', UNKNOWN_OPTION = 'UNKNOWN_OPTION', UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY', UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT', UNUSED_EXTERNAL_IMPORT = 'UNUSED_EXTERNAL_IMPORT', VALIDATION_ERROR = 'VALIDATION_ERROR';
|
||||||
function logAddonNotGenerated(message, hook, plugin) {
|
function logAddonNotGenerated(message, hook, plugin) {
|
||||||
return {
|
return {
|
||||||
code: ADDON_ERROR,
|
code: ADDON_ERROR,
|
||||||
|
|
@ -829,6 +831,13 @@ function logNamespaceConflict(binding, reexportingModuleId, sources) {
|
||||||
reexporter: reexportingModuleId
|
reexporter: reexportingModuleId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function logNonExternalSourcePhaseImport(source, importer) {
|
||||||
|
return {
|
||||||
|
code: NON_EXTERNAL_SOURCE_PHASE_IMPORT,
|
||||||
|
message: `Source phase import "${source}" in "${relativeId(importer)}" must be external. Source phase imports are only supported for external modules. Use the "external" option to mark this module as external.`,
|
||||||
|
url: getRollupUrl(URL_SOURCE_PHASE_IMPORTS)
|
||||||
|
};
|
||||||
|
}
|
||||||
function logNoTransformMapOrAstWithoutCode(pluginName) {
|
function logNoTransformMapOrAstWithoutCode(pluginName) {
|
||||||
return {
|
return {
|
||||||
code: NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE,
|
code: NO_TRANSFORM_MAP_OR_AST_WITHOUT_CODE,
|
||||||
|
|
@ -906,6 +915,13 @@ function logShimmedExport(id, binding) {
|
||||||
message: `Missing export "${binding}" has been shimmed in module "${relativeId(id)}".`
|
message: `Missing export "${binding}" has been shimmed in module "${relativeId(id)}".`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function logSourcePhaseFormatUnsupported(outputFormat, chunkId, dependencyId) {
|
||||||
|
return {
|
||||||
|
code: SOURCE_PHASE_FORMAT_UNSUPPORTED,
|
||||||
|
message: `Source phase imports are not supported for the "${outputFormat}" output format, importing "${dependencyId}" in "${chunkId}". Use the "es" output format to support source phase imports.`,
|
||||||
|
url: getRollupUrl(URL_SOURCE_PHASE_IMPORTS)
|
||||||
|
};
|
||||||
|
}
|
||||||
function logSourcemapBroken(plugin) {
|
function logSourcemapBroken(plugin) {
|
||||||
return {
|
return {
|
||||||
code: SOURCEMAP_BROKEN,
|
code: SOURCEMAP_BROKEN,
|
||||||
|
|
@ -1143,7 +1159,9 @@ const FIXED_STRINGS = [
|
||||||
'noSideEffects',
|
'noSideEffects',
|
||||||
'sourcemap',
|
'sourcemap',
|
||||||
'using',
|
'using',
|
||||||
'await using'
|
'await using',
|
||||||
|
'source',
|
||||||
|
'defer'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ANNOTATION_KEY = '_rollupAnnotations';
|
const ANNOTATION_KEY = '_rollupAnnotations';
|
||||||
|
|
@ -1559,13 +1577,15 @@ const nodeConverters = [
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importDeclaration(position, buffer) {
|
function importDeclaration(position, buffer) {
|
||||||
|
const phaseIndex = buffer[position + 5];
|
||||||
return {
|
return {
|
||||||
type: 'ImportDeclaration',
|
type: 'ImportDeclaration',
|
||||||
start: buffer[position],
|
start: buffer[position],
|
||||||
end: buffer[position + 1],
|
end: buffer[position + 1],
|
||||||
specifiers: convertNodeList(buffer[position + 2], buffer),
|
specifiers: convertNodeList(buffer[position + 2], buffer),
|
||||||
source: convertNode(buffer[position + 3], buffer),
|
source: convertNode(buffer[position + 3], buffer),
|
||||||
attributes: convertNodeList(buffer[position + 4], buffer)
|
attributes: convertNodeList(buffer[position + 4], buffer),
|
||||||
|
...(phaseIndex === 0 ? {} : { phase: FIXED_STRINGS[phaseIndex] })
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importDefaultSpecifier(position, buffer) {
|
function importDefaultSpecifier(position, buffer) {
|
||||||
|
|
@ -1578,12 +1598,14 @@ const nodeConverters = [
|
||||||
},
|
},
|
||||||
function importExpression(position, buffer) {
|
function importExpression(position, buffer) {
|
||||||
const optionsPosition = buffer[position + 3];
|
const optionsPosition = buffer[position + 3];
|
||||||
|
const phaseIndex = buffer[position + 4];
|
||||||
return {
|
return {
|
||||||
type: 'ImportExpression',
|
type: 'ImportExpression',
|
||||||
start: buffer[position],
|
start: buffer[position],
|
||||||
end: buffer[position + 1],
|
end: buffer[position + 1],
|
||||||
source: convertNode(buffer[position + 2], buffer),
|
source: convertNode(buffer[position + 2], buffer),
|
||||||
options: optionsPosition === 0 ? null : convertNode(optionsPosition, buffer)
|
options: optionsPosition === 0 ? null : convertNode(optionsPosition, buffer),
|
||||||
|
...(phaseIndex === 0 ? {} : { phase: FIXED_STRINGS[phaseIndex] })
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
function importNamespaceSpecifier(position, buffer) {
|
function importNamespaceSpecifier(position, buffer) {
|
||||||
|
|
@ -2309,6 +2331,7 @@ exports.logModuleParseError = logModuleParseError;
|
||||||
exports.logNamespaceConflict = logNamespaceConflict;
|
exports.logNamespaceConflict = logNamespaceConflict;
|
||||||
exports.logNoAssetSourceSet = logNoAssetSourceSet;
|
exports.logNoAssetSourceSet = logNoAssetSourceSet;
|
||||||
exports.logNoTransformMapOrAstWithoutCode = logNoTransformMapOrAstWithoutCode;
|
exports.logNoTransformMapOrAstWithoutCode = logNoTransformMapOrAstWithoutCode;
|
||||||
|
exports.logNonExternalSourcePhaseImport = logNonExternalSourcePhaseImport;
|
||||||
exports.logOnlyInlineSourcemapsForStdout = logOnlyInlineSourcemapsForStdout;
|
exports.logOnlyInlineSourcemapsForStdout = logOnlyInlineSourcemapsForStdout;
|
||||||
exports.logOptimizeChunkStatus = logOptimizeChunkStatus;
|
exports.logOptimizeChunkStatus = logOptimizeChunkStatus;
|
||||||
exports.logParseError = logParseError;
|
exports.logParseError = logParseError;
|
||||||
|
|
@ -2316,6 +2339,7 @@ exports.logPluginError = logPluginError;
|
||||||
exports.logRedeclarationError = logRedeclarationError;
|
exports.logRedeclarationError = logRedeclarationError;
|
||||||
exports.logReservedNamespace = logReservedNamespace;
|
exports.logReservedNamespace = logReservedNamespace;
|
||||||
exports.logShimmedExport = logShimmedExport;
|
exports.logShimmedExport = logShimmedExport;
|
||||||
|
exports.logSourcePhaseFormatUnsupported = logSourcePhaseFormatUnsupported;
|
||||||
exports.logSourcemapBroken = logSourcemapBroken;
|
exports.logSourcemapBroken = logSourcemapBroken;
|
||||||
exports.logSyntheticNamedExportsNeedNamespaceExport = logSyntheticNamedExportsNeedNamespaceExport;
|
exports.logSyntheticNamedExportsNeedNamespaceExport = logSyntheticNamedExportsNeedNamespaceExport;
|
||||||
exports.logThisIsUndefined = logThisIsUndefined;
|
exports.logThisIsUndefined = logThisIsUndefined;
|
||||||
|
|
|
||||||
95
frontend/node_modules/rollup/dist/shared/rollup.js
generated
vendored
95
frontend/node_modules/rollup/dist/shared/rollup.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ function _mergeNamespaces(n, m) {
|
||||||
|
|
||||||
const promises__namespace = /*#__PURE__*/_interopNamespaceDefault(promises);
|
const promises__namespace = /*#__PURE__*/_interopNamespaceDefault(promises);
|
||||||
|
|
||||||
var version = "4.59.1";
|
var version = "4.60.0";
|
||||||
|
|
||||||
function ensureArray$1(items) {
|
function ensureArray$1(items) {
|
||||||
if (Array.isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
|
|
@ -6255,12 +6255,15 @@ class Variable extends ExpressionEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Synthetic import name for source phase imports, similar to '*' for namespaces */
|
||||||
|
const SOURCE_PHASE_IMPORT = '*source';
|
||||||
class ExternalVariable extends Variable {
|
class ExternalVariable extends Variable {
|
||||||
constructor(module, name) {
|
constructor(module, name) {
|
||||||
super(name);
|
super(name);
|
||||||
this.referenced = false;
|
this.referenced = false;
|
||||||
this.module = module;
|
this.module = module;
|
||||||
this.isNamespace = name === '*';
|
this.isNamespace = name === '*';
|
||||||
|
this.isSourcePhase = name === SOURCE_PHASE_IMPORT;
|
||||||
}
|
}
|
||||||
addReference(identifier) {
|
addReference(identifier) {
|
||||||
this.referenced = true;
|
this.referenced = true;
|
||||||
|
|
@ -12209,6 +12212,13 @@ function getInteropBlock(dependencies, interop, externalLiveBindings, freeze, sy
|
||||||
return `${getHelpersBlock(neededInteropHelpers, accessedGlobals, indent, snippets, externalLiveBindings, freeze, symbols)}${interopStatements.length > 0 ? `${interopStatements.join(n)}${n}${n}` : ''}`;
|
return `${getHelpersBlock(neededInteropHelpers, accessedGlobals, indent, snippets, externalLiveBindings, freeze, symbols)}${interopStatements.length > 0 ? `${interopStatements.join(n)}${n}${n}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throwOnPhase(outputFormat, chunkId, dependencies) {
|
||||||
|
const sourcePhaseDependency = dependencies.find(dependency => dependency.sourcePhaseImport);
|
||||||
|
if (sourcePhaseDependency) {
|
||||||
|
parseAst_js.error(parseAst_js.logSourcePhaseFormatUnsupported(outputFormat, chunkId, sourcePhaseDependency.importPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addJsExtension(name) {
|
function addJsExtension(name) {
|
||||||
return name.endsWith('.js') ? name : name + '.js';
|
return name.endsWith('.js') ? name : name + '.js';
|
||||||
}
|
}
|
||||||
|
|
@ -12347,6 +12357,7 @@ function warnOnBuiltins(log, dependencies) {
|
||||||
|
|
||||||
function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, log, outro, snippets }, { amd, esModule, externalLiveBindings, freeze, generatedCode: { symbols }, interop, reexportProtoFromExternal, strict }) {
|
function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, log, outro, snippets }, { amd, esModule, externalLiveBindings, freeze, generatedCode: { symbols }, interop, reexportProtoFromExternal, strict }) {
|
||||||
warnOnBuiltins(log, dependencies);
|
warnOnBuiltins(log, dependencies);
|
||||||
|
throwOnPhase('amd', id, dependencies);
|
||||||
const deps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
const deps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
||||||
const parameters = dependencies.map(m => m.name);
|
const parameters = dependencies.map(m => m.name);
|
||||||
const { n, getNonArrowFunctionIntro, _ } = snippets;
|
const { n, getNonArrowFunctionIntro, _ } = snippets;
|
||||||
|
|
@ -12384,7 +12395,8 @@ function amd(magicString, { accessedGlobals, dependencies, exports: exports$1, h
|
||||||
.append(`${n}${n}}));`);
|
.append(`${n}${n}}));`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cjs(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, outro, snippets }, { compact, esModule, externalLiveBindings, freeze, interop, generatedCode: { symbols }, reexportProtoFromExternal, strict }) {
|
function cjs(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, isEntryFacade, isModuleFacade, namedExportsMode, outro, snippets }, { compact, esModule, externalLiveBindings, freeze, interop, generatedCode: { symbols }, reexportProtoFromExternal, strict }) {
|
||||||
|
throwOnPhase('cjs', id, dependencies);
|
||||||
const { _, n } = snippets;
|
const { _, n } = snippets;
|
||||||
const useStrict = strict ? `'use strict';${n}${n}` : '';
|
const useStrict = strict ? `'use strict';${n}${n}` : '';
|
||||||
let namespaceMarkers = getNamespaceMarkers(namedExportsMode && hasExports, isEntryFacade && (esModule === true || (esModule === 'if-default-prop' && hasDefaultExport)), isModuleFacade && symbols, snippets);
|
let namespaceMarkers = getNamespaceMarkers(namedExportsMode && hasExports, isEntryFacade && (esModule === true || (esModule === 'if-default-prop' && hasDefaultExport)), isModuleFacade && symbols, snippets);
|
||||||
|
|
@ -12437,11 +12449,16 @@ function es(magicString, { accessedGlobals, indent: t, intro, outro, dependencie
|
||||||
}
|
}
|
||||||
function getImportBlock(dependencies, importAttributesKey, { _ }) {
|
function getImportBlock(dependencies, importAttributesKey, { _ }) {
|
||||||
const importBlock = [];
|
const importBlock = [];
|
||||||
for (const { importPath, reexports, imports, name, attributes } of dependencies) {
|
for (const { importPath, reexports, imports, name, attributes, sourcePhaseImport } of dependencies) {
|
||||||
const assertion = attributes ? `${_}${importAttributesKey}${_}${attributes}` : '';
|
const assertion = attributes ? `${_}${importAttributesKey}${_}${attributes}` : '';
|
||||||
const pathWithAssertion = `'${importPath}'${assertion};`;
|
const pathWithAssertion = `'${importPath}'${assertion};`;
|
||||||
|
if (sourcePhaseImport) {
|
||||||
|
importBlock.push(`import source ${sourcePhaseImport} from${_}${pathWithAssertion}`);
|
||||||
|
}
|
||||||
if (!reexports && !imports) {
|
if (!reexports && !imports) {
|
||||||
importBlock.push(`import${_}${pathWithAssertion}`);
|
if (!sourcePhaseImport) {
|
||||||
|
importBlock.push(`import${_}${pathWithAssertion}`);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (imports) {
|
if (imports) {
|
||||||
|
|
@ -12595,7 +12612,8 @@ function trimEmptyImports(dependencies) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function iife(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, indent: t, intro, namedExportsMode, log, outro, snippets }, { compact, esModule, extend, freeze, externalLiveBindings, reexportProtoFromExternal, globals, interop, name, generatedCode: { symbols }, strict }) {
|
function iife(magicString, { accessedGlobals, dependencies, exports: exports$1, hasDefaultExport, hasExports, id, indent: t, intro, namedExportsMode, log, outro, snippets }, { compact, esModule, extend, freeze, externalLiveBindings, reexportProtoFromExternal, globals, interop, name, generatedCode: { symbols }, strict }) {
|
||||||
|
throwOnPhase('iife', id, dependencies);
|
||||||
const { _, getNonArrowFunctionIntro, getPropertyAccess, n } = snippets;
|
const { _, getNonArrowFunctionIntro, getPropertyAccess, n } = snippets;
|
||||||
const isNamespaced = name && name.includes('.');
|
const isNamespaced = name && name.includes('.');
|
||||||
const useVariableAssignment = !extend && !isNamespaced;
|
const useVariableAssignment = !extend && !isNamespaced;
|
||||||
|
|
@ -12654,7 +12672,8 @@ function iife(magicString, { accessedGlobals, dependencies, exports: exports$1,
|
||||||
|
|
||||||
const MISSING_EXPORT_SHIM_VARIABLE = '_missingExportShim';
|
const MISSING_EXPORT_SHIM_VARIABLE = '_missingExportShim';
|
||||||
|
|
||||||
function system(magicString, { accessedGlobals, dependencies, exports: exports$1, hasExports, indent: t, intro, snippets, outro, usesTopLevelAwait }, { externalLiveBindings, freeze, name, generatedCode: { symbols }, strict, systemNullSetters }) {
|
function system(magicString, { accessedGlobals, dependencies, exports: exports$1, hasExports, id, indent: t, intro, snippets, outro, usesTopLevelAwait }, { externalLiveBindings, freeze, name, generatedCode: { symbols }, strict, systemNullSetters }) {
|
||||||
|
throwOnPhase('system', id, dependencies);
|
||||||
const { _, getFunctionIntro, getNonArrowFunctionIntro, n, s } = snippets;
|
const { _, getFunctionIntro, getNonArrowFunctionIntro, n, s } = snippets;
|
||||||
const { importBindings, setters, starExcludes } = analyzeDependencies(dependencies, exports$1, t, snippets);
|
const { importBindings, setters, starExcludes } = analyzeDependencies(dependencies, exports$1, t, snippets);
|
||||||
const registeredName = name ? `'${name}',${_}` : '';
|
const registeredName = name ? `'${name}',${_}` : '';
|
||||||
|
|
@ -12822,6 +12841,7 @@ function umd(magicString, { accessedGlobals, dependencies, exports: exports$1, h
|
||||||
if (hasExports && !name) {
|
if (hasExports && !name) {
|
||||||
return parseAst_js.error(parseAst_js.logMissingNameOptionForUmdExport());
|
return parseAst_js.error(parseAst_js.logMissingNameOptionForUmdExport());
|
||||||
}
|
}
|
||||||
|
throwOnPhase('umd', id, dependencies);
|
||||||
warnOnBuiltins(log, dependencies);
|
warnOnBuiltins(log, dependencies);
|
||||||
const amdDeps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
const amdDeps = dependencies.map(m => `'${updateExtensionForRelativeAmdId(m.importPath, amd.forceJsExtensionForImports)}'`);
|
||||||
const cjsDeps = dependencies.map(m => `require('${m.importPath}')`);
|
const cjsDeps = dependencies.map(m => `require('${m.importPath}')`);
|
||||||
|
|
@ -17730,6 +17750,8 @@ const bufferParsers = [
|
||||||
node.specifiers = convertNodeList(node, scope, buffer[position], buffer);
|
node.specifiers = convertNodeList(node, scope, buffer[position], buffer);
|
||||||
node.source = convertNode(node, scope, buffer[position + 1], buffer);
|
node.source = convertNode(node, scope, buffer[position + 1], buffer);
|
||||||
node.attributes = convertNodeList(node, scope, buffer[position + 2], buffer);
|
node.attributes = convertNodeList(node, scope, buffer[position + 2], buffer);
|
||||||
|
const phaseIndex = buffer[position + 3];
|
||||||
|
node.phase = phaseIndex === 0 ? undefined : parseAst_js.FIXED_STRINGS[phaseIndex];
|
||||||
},
|
},
|
||||||
function importDefaultSpecifier(node, position, buffer) {
|
function importDefaultSpecifier(node, position, buffer) {
|
||||||
const { scope } = node;
|
const { scope } = node;
|
||||||
|
|
@ -17741,6 +17763,8 @@ const bufferParsers = [
|
||||||
node.sourceAstNode = parseAst_js.convertNode(buffer[position], buffer);
|
node.sourceAstNode = parseAst_js.convertNode(buffer[position], buffer);
|
||||||
const optionsPosition = buffer[position + 1];
|
const optionsPosition = buffer[position + 1];
|
||||||
node.options = optionsPosition === 0 ? null : convertNode(node, scope, optionsPosition, buffer);
|
node.options = optionsPosition === 0 ? null : convertNode(node, scope, optionsPosition, buffer);
|
||||||
|
const phaseIndex = buffer[position + 2];
|
||||||
|
node.phase = phaseIndex === 0 ? undefined : parseAst_js.FIXED_STRINGS[phaseIndex];
|
||||||
},
|
},
|
||||||
function importNamespaceSpecifier(node, position, buffer) {
|
function importNamespaceSpecifier(node, position, buffer) {
|
||||||
const { scope } = node;
|
const { scope } = node;
|
||||||
|
|
@ -18519,6 +18543,7 @@ class Module {
|
||||||
this.isUserDefinedEntryPoint = false;
|
this.isUserDefinedEntryPoint = false;
|
||||||
this.needsExportShim = false;
|
this.needsExportShim = false;
|
||||||
this.sideEffectDependenciesByVariable = new Map();
|
this.sideEffectDependenciesByVariable = new Map();
|
||||||
|
this.sourcePhaseSources = new Set();
|
||||||
this.sourcesWithAttributes = new Map();
|
this.sourcesWithAttributes = new Map();
|
||||||
this.allExportsIncluded = false;
|
this.allExportsIncluded = false;
|
||||||
this.ast = null;
|
this.ast = null;
|
||||||
|
|
@ -19169,16 +19194,19 @@ class Module {
|
||||||
if (this.scope.variables.has(localName) || this.importDescriptions.has(localName)) {
|
if (this.scope.variables.has(localName) || this.importDescriptions.has(localName)) {
|
||||||
this.error(parseAst_js.logRedeclarationError(localName), specifier.local.start);
|
this.error(parseAst_js.logRedeclarationError(localName), specifier.local.start);
|
||||||
}
|
}
|
||||||
const name = specifier instanceof ImportDefaultSpecifier
|
const name = node.phase === 'source'
|
||||||
? 'default'
|
? SOURCE_PHASE_IMPORT
|
||||||
: specifier instanceof ImportNamespaceSpecifier
|
: specifier instanceof ImportDefaultSpecifier
|
||||||
? '*'
|
? 'default'
|
||||||
: specifier.imported instanceof Identifier
|
: specifier instanceof ImportNamespaceSpecifier
|
||||||
? specifier.imported.name
|
? '*'
|
||||||
: specifier.imported.value;
|
: specifier.imported instanceof Identifier
|
||||||
|
? specifier.imported.name
|
||||||
|
: specifier.imported.value;
|
||||||
this.importDescriptions.set(localName, {
|
this.importDescriptions.set(localName, {
|
||||||
module: null, // filled in later
|
module: null, // filled in later
|
||||||
name,
|
name,
|
||||||
|
phase: node.phase === 'source' ? 'source' : 'instance',
|
||||||
source,
|
source,
|
||||||
start: specifier.start
|
start: specifier.start
|
||||||
});
|
});
|
||||||
|
|
@ -19251,6 +19279,9 @@ class Module {
|
||||||
else {
|
else {
|
||||||
this.sourcesWithAttributes.set(source, parsedAttributes);
|
this.sourcesWithAttributes.set(source, parsedAttributes);
|
||||||
}
|
}
|
||||||
|
if (declaration.phase === 'source') {
|
||||||
|
this.sourcePhaseSources.add(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getImportedJsxFactoryVariable(baseName, nodeStart, importSource) {
|
getImportedJsxFactoryVariable(baseName, nodeStart, importSource) {
|
||||||
const { id } = this.resolvedIds[importSource];
|
const { id } = this.resolvedIds[importSource];
|
||||||
|
|
@ -19464,6 +19495,9 @@ function deconflictImportsEsmOrSystem(usedNames, imports, dependenciesToBeDeconf
|
||||||
? externalChunkByModule.get(module)
|
? externalChunkByModule.get(module)
|
||||||
: chunkByModule.get(module)).variableName);
|
: chunkByModule.get(module)).variableName);
|
||||||
}
|
}
|
||||||
|
else if (module instanceof ExternalModule && variable.isSourcePhase) {
|
||||||
|
variable.setRenderNames(null, getSafeName(module.suggestedVariableName + '__source', usedNames, variable.forbiddenNames));
|
||||||
|
}
|
||||||
else if (module instanceof ExternalModule && name === 'default') {
|
else if (module instanceof ExternalModule && name === 'default') {
|
||||||
variable.setRenderNames(null, getSafeName([...module.exportedVariables].some(([exportedVariable, exportedName]) => exportedName === '*' && exportedVariable.included)
|
variable.setRenderNames(null, getSafeName([...module.exportedVariables].some(([exportedVariable, exportedName]) => exportedName === '*' && exportedVariable.included)
|
||||||
? module.suggestedVariableName + '__default'
|
? module.suggestedVariableName + '__default'
|
||||||
|
|
@ -20401,10 +20435,14 @@ class Chunk {
|
||||||
const module = variable.module;
|
const module = variable.module;
|
||||||
let dependency;
|
let dependency;
|
||||||
let imported;
|
let imported;
|
||||||
|
const isSourcePhase = module instanceof ExternalModule && variable.isSourcePhase;
|
||||||
if (module instanceof ExternalModule) {
|
if (module instanceof ExternalModule) {
|
||||||
dependency = this.externalChunkByModule.get(module);
|
dependency = this.externalChunkByModule.get(module);
|
||||||
imported = variable.name;
|
imported = variable.name;
|
||||||
if (imported !== 'default' && imported !== '*' && interop(module.id) === 'defaultOnly') {
|
if (!isSourcePhase &&
|
||||||
|
imported !== 'default' &&
|
||||||
|
imported !== '*' &&
|
||||||
|
interop(module.id) === 'defaultOnly') {
|
||||||
return parseAst_js.error(parseAst_js.logUnexpectedNamedImport(module.id, imported, false));
|
return parseAst_js.error(parseAst_js.logUnexpectedNamedImport(module.id, imported, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20414,7 +20452,8 @@ class Chunk {
|
||||||
}
|
}
|
||||||
getOrCreate(importsByDependency, dependency, getNewArray).push({
|
getOrCreate(importsByDependency, dependency, getNewArray).push({
|
||||||
imported,
|
imported,
|
||||||
local: variable.getName(this.snippets.getPropertyAccess)
|
local: variable.getName(this.snippets.getPropertyAccess),
|
||||||
|
phase: isSourcePhase ? 'source' : 'instance'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return importsByDependency;
|
return importsByDependency;
|
||||||
|
|
@ -20565,6 +20604,9 @@ class Chunk {
|
||||||
const reexports = reexportSpecifiers.get(dependency) || null;
|
const reexports = reexportSpecifiers.get(dependency) || null;
|
||||||
const namedExportsMode = dependency instanceof ExternalChunk || dependency.exportMode !== 'default';
|
const namedExportsMode = dependency instanceof ExternalChunk || dependency.exportMode !== 'default';
|
||||||
const importPath = dependency.getImportPath(fileName);
|
const importPath = dependency.getImportPath(fileName);
|
||||||
|
// Separate source-phase imports from regular imports
|
||||||
|
const sourcePhaseImport = imports?.find(index => index.phase === 'source');
|
||||||
|
const instanceImports = imports?.filter(index => index.phase !== 'source') ?? null;
|
||||||
renderedDependencies.set(dependency, {
|
renderedDependencies.set(dependency, {
|
||||||
attributes: dependency instanceof ExternalChunk
|
attributes: dependency instanceof ExternalChunk
|
||||||
? dependency.getImportAttributes(this.snippets)
|
? dependency.getImportAttributes(this.snippets)
|
||||||
|
|
@ -20574,12 +20616,13 @@ class Chunk {
|
||||||
(this.outputOptions.format === 'umd' || this.outputOptions.format === 'iife') &&
|
(this.outputOptions.format === 'umd' || this.outputOptions.format === 'iife') &&
|
||||||
getGlobalName(dependency, this.outputOptions.globals, (imports || reexports) !== null, this.inputOptions.onLog),
|
getGlobalName(dependency, this.outputOptions.globals, (imports || reexports) !== null, this.inputOptions.onLog),
|
||||||
importPath,
|
importPath,
|
||||||
imports,
|
imports: instanceImports && instanceImports.length > 0 ? instanceImports : null,
|
||||||
isChunk: dependency instanceof Chunk,
|
isChunk: dependency instanceof Chunk,
|
||||||
name: dependency.variableName,
|
name: dependency.variableName,
|
||||||
namedExportsMode,
|
namedExportsMode,
|
||||||
namespaceVariableName: dependency.namespaceVariableName,
|
namespaceVariableName: dependency.namespaceVariableName,
|
||||||
reexports
|
reexports,
|
||||||
|
sourcePhaseImport: sourcePhaseImport?.local
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (this.renderedDependencies = renderedDependencies);
|
return (this.renderedDependencies = renderedDependencies);
|
||||||
|
|
@ -22748,13 +22791,16 @@ class ModuleLoader {
|
||||||
return loadNewModulesPromise;
|
return loadNewModulesPromise;
|
||||||
}
|
}
|
||||||
async fetchDynamicDependencies(module, resolveDynamicImportPromises) {
|
async fetchDynamicDependencies(module, resolveDynamicImportPromises) {
|
||||||
const dependencies = await Promise.all(resolveDynamicImportPromises.map(resolveDynamicImportPromise => resolveDynamicImportPromise.then(async ([{ node }, resolvedId]) => {
|
const dependencies = await Promise.all(resolveDynamicImportPromises.map(resolveDynamicImportPromise => resolveDynamicImportPromise.then(async ([{ argument, node }, resolvedId]) => {
|
||||||
if (resolvedId === null)
|
if (resolvedId === null)
|
||||||
return null;
|
return null;
|
||||||
if (typeof resolvedId === 'string') {
|
if (typeof resolvedId === 'string') {
|
||||||
node.resolution = resolvedId;
|
node.resolution = resolvedId;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (node.phase === 'source' && !resolvedId.external) {
|
||||||
|
return parseAst_js.error(parseAst_js.logNonExternalSourcePhaseImport(typeof argument === 'string' ? argument : parseAst_js.relativeId(resolvedId.id), module.id));
|
||||||
|
}
|
||||||
return (node.resolution = await this.fetchResolvedDependency(parseAst_js.relativeId(resolvedId.id), module.id, resolvedId));
|
return (node.resolution = await this.fetchResolvedDependency(parseAst_js.relativeId(resolvedId.id), module.id, resolvedId));
|
||||||
})));
|
})));
|
||||||
for (const dependency of dependencies) {
|
for (const dependency of dependencies) {
|
||||||
|
|
@ -22834,7 +22880,12 @@ class ModuleLoader {
|
||||||
return this.fetchModule(resolvedId, importer, false, false);
|
return this.fetchModule(resolvedId, importer, false, false);
|
||||||
}
|
}
|
||||||
async fetchStaticDependencies(module, resolveStaticDependencyPromises) {
|
async fetchStaticDependencies(module, resolveStaticDependencyPromises) {
|
||||||
for (const dependency of await Promise.all(resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => resolveStaticDependencyPromise.then(([source, resolvedId]) => this.fetchResolvedDependency(source, module.id, resolvedId))))) {
|
for (const dependency of await Promise.all(resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => resolveStaticDependencyPromise.then(([source, resolvedId]) => {
|
||||||
|
if (module.sourcePhaseSources.has(source) && !resolvedId.external) {
|
||||||
|
return parseAst_js.error(parseAst_js.logNonExternalSourcePhaseImport(source, module.id));
|
||||||
|
}
|
||||||
|
return this.fetchResolvedDependency(source, module.id, resolvedId);
|
||||||
|
})))) {
|
||||||
module.dependencies.add(dependency);
|
module.dependencies.add(dependency);
|
||||||
dependency.importers.push(module.id);
|
dependency.importers.push(module.id);
|
||||||
}
|
}
|
||||||
|
|
@ -23246,7 +23297,7 @@ class Graph {
|
||||||
warnForMissingExports() {
|
warnForMissingExports() {
|
||||||
for (const module of this.modules) {
|
for (const module of this.modules) {
|
||||||
for (const importDescription of module.importDescriptions.values()) {
|
for (const importDescription of module.importDescriptions.values()) {
|
||||||
if (importDescription.name !== '*') {
|
if (importDescription.name !== '*' && importDescription.phase !== 'source') {
|
||||||
const [variable, options] = importDescription.module.getVariableForExportName(importDescription.name, { importChain: [module.id] });
|
const [variable, options] = importDescription.module.getVariableForExportName(importDescription.name, { importChain: [module.id] });
|
||||||
if (!variable) {
|
if (!variable) {
|
||||||
module.log(parseAst_js.LOGLEVEL_WARN, parseAst_js.logMissingExport(importDescription.name, module.id, importDescription.module.id, !!options?.missingButExportExists), importDescription.start);
|
module.log(parseAst_js.LOGLEVEL_WARN, parseAst_js.logMissingExport(importDescription.name, module.id, importDescription.module.id, !!options?.missingButExportExists), importDescription.start);
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/shared/watch-cli.js
generated
vendored
4
frontend/node_modules/rollup/dist/shared/watch-cli.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/node_modules/rollup/dist/shared/watch.js
generated
vendored
4
frontend/node_modules/rollup/dist/shared/watch.js
generated
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
@license
|
@license
|
||||||
Rollup.js v4.59.1
|
Rollup.js v4.60.0
|
||||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
Sun, 22 Mar 2026 06:57:22 GMT - commit 6ecd69fb2ce736c8aabb50829edd227d1792c957
|
||||||
|
|
||||||
https://github.com/rollup/rollup
|
https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
|
|
||||||
55
frontend/node_modules/rollup/package.json
generated
vendored
55
frontend/node_modules/rollup/package.json
generated
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "rollup",
|
"name": "rollup",
|
||||||
"version": "4.59.1",
|
"version": "4.60.0",
|
||||||
"description": "Next-generation ES module bundler",
|
"description": "Next-generation ES module bundler",
|
||||||
"main": "dist/rollup.js",
|
"main": "dist/rollup.js",
|
||||||
"module": "dist/es/rollup.js",
|
"module": "dist/es/rollup.js",
|
||||||
|
|
@ -114,31 +114,31 @@
|
||||||
"homepage": "https://rollupjs.org/",
|
"homepage": "https://rollupjs.org/",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "~2.3.2",
|
"fsevents": "~2.3.2",
|
||||||
"@rollup/rollup-darwin-arm64": "4.59.1",
|
"@rollup/rollup-darwin-arm64": "4.60.0",
|
||||||
"@rollup/rollup-android-arm64": "4.59.1",
|
"@rollup/rollup-android-arm64": "4.60.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.59.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.60.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.59.1",
|
"@rollup/rollup-freebsd-arm64": "4.60.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.59.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.59.1",
|
"@rollup/rollup-linux-arm64-musl": "4.60.0",
|
||||||
"@rollup/rollup-android-arm-eabi": "4.59.1",
|
"@rollup/rollup-android-arm-eabi": "4.60.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.60.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.59.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.60.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.59.1",
|
"@rollup/rollup-linux-loong64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-linux-loong64-musl": "4.59.1",
|
"@rollup/rollup-linux-loong64-musl": "4.60.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.59.1",
|
"@rollup/rollup-linux-riscv64-musl": "4.60.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.1",
|
"@rollup/rollup-linux-ppc64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-linux-ppc64-musl": "4.59.1",
|
"@rollup/rollup-linux-ppc64-musl": "4.60.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.59.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.60.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.59.1",
|
"@rollup/rollup-darwin-x64": "4.60.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.59.1",
|
"@rollup/rollup-win32-x64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.59.1",
|
"@rollup/rollup-win32-x64-msvc": "4.60.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.59.1",
|
"@rollup/rollup-freebsd-x64": "4.60.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.59.1",
|
"@rollup/rollup-linux-x64-gnu": "4.60.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.59.1",
|
"@rollup/rollup-linux-x64-musl": "4.60.0",
|
||||||
"@rollup/rollup-openbsd-x64": "4.59.1",
|
"@rollup/rollup-openbsd-x64": "4.60.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.59.1"
|
"@rollup/rollup-openharmony-arm64": "4.60.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
|
|
@ -176,6 +176,7 @@
|
||||||
"@vue/language-server": "^3.2.5",
|
"@vue/language-server": "^3.2.5",
|
||||||
"acorn": "^8.16.0",
|
"acorn": "^8.16.0",
|
||||||
"acorn-import-assertions": "^1.9.0",
|
"acorn-import-assertions": "^1.9.0",
|
||||||
|
"acorn-import-phases": "^1.0.4",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
"buble": "^0.20.0",
|
"buble": "^0.20.0",
|
||||||
"builtin-modules": "^5.0.0",
|
"builtin-modules": "^5.0.0",
|
||||||
|
|
@ -186,7 +187,7 @@
|
||||||
"date-time": "^4.0.0",
|
"date-time": "^4.0.0",
|
||||||
"es5-shim": "^4.6.7",
|
"es5-shim": "^4.6.7",
|
||||||
"es6-shim": "^0.35.8",
|
"es6-shim": "^0.35.8",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.1.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-unicorn": "^63.0.0",
|
"eslint-plugin-unicorn": "^63.0.0",
|
||||||
|
|
|
||||||
2
frontend/node_modules/svelte/compiler/index.js
generated
vendored
2
frontend/node_modules/svelte/compiler/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
frontend/node_modules/svelte/package.json
generated
vendored
2
frontend/node_modules/svelte/package.json
generated
vendored
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "svelte",
|
"name": "svelte",
|
||||||
"description": "Cybernetically enhanced web apps",
|
"description": "Cybernetically enhanced web apps",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "5.54.0",
|
"version": "5.55.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types/index.d.ts",
|
"types": "./types/index.d.ts",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
36
frontend/node_modules/svelte/src/compiler/phases/2-analyze/index.js
generated
vendored
36
frontend/node_modules/svelte/src/compiler/phases/2-analyze/index.js
generated
vendored
|
|
@ -1074,6 +1074,9 @@ function calculate_blockers(instance, analysis) {
|
||||||
|
|
||||||
let awaited = false;
|
let awaited = false;
|
||||||
|
|
||||||
|
/** @type {Array<ESTree.Statement | ESTree.VariableDeclarator>} */
|
||||||
|
let sync_group = [];
|
||||||
|
|
||||||
// TODO this should probably be attached to the scope?
|
// TODO this should probably be attached to the scope?
|
||||||
const promises = b.id('$$promises');
|
const promises = b.id('$$promises');
|
||||||
|
|
||||||
|
|
@ -1088,6 +1091,13 @@ function calculate_blockers(instance, analysis) {
|
||||||
binding.blocker = blocker;
|
binding.blocker = blocker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flush_sync_group() {
|
||||||
|
if (sync_group.length === 0) return;
|
||||||
|
|
||||||
|
analysis.instance_body.async.push({ nodes: sync_group, has_await: false });
|
||||||
|
sync_group = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
|
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
|
||||||
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
|
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
|
||||||
|
|
@ -1149,6 +1159,9 @@ function calculate_blockers(instance, analysis) {
|
||||||
|
|
||||||
trace_references(declarator, reads, writes, instance.scope);
|
trace_references(declarator, reads, writes, instance.scope);
|
||||||
|
|
||||||
|
// Needs to happen before blocker computation
|
||||||
|
if (has_await) flush_sync_group();
|
||||||
|
|
||||||
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
|
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
|
||||||
b.member(promises, b.literal(analysis.instance_body.async.length), true)
|
b.member(promises, b.literal(analysis.instance_body.async.length), true)
|
||||||
);
|
);
|
||||||
|
|
@ -1161,11 +1174,12 @@ function calculate_blockers(instance, analysis) {
|
||||||
push_declaration(id, blocker);
|
push_declaration(id, blocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// one declarator per declaration, makes things simpler
|
if (has_await) {
|
||||||
analysis.instance_body.async.push({
|
// one declarator per declaration, makes things simpler
|
||||||
node: declarator,
|
analysis.instance_body.async.push({ nodes: [declarator], has_await: true });
|
||||||
has_await
|
} else {
|
||||||
});
|
sync_group.push(declarator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (awaited) {
|
} else if (awaited) {
|
||||||
|
|
@ -1177,6 +1191,9 @@ function calculate_blockers(instance, analysis) {
|
||||||
|
|
||||||
trace_references(node, reads, writes, instance.scope);
|
trace_references(node, reads, writes, instance.scope);
|
||||||
|
|
||||||
|
// Needs to happen before blocker computation
|
||||||
|
if (has_await) flush_sync_group();
|
||||||
|
|
||||||
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
|
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
|
||||||
b.member(promises, b.literal(analysis.instance_body.async.length), true)
|
b.member(promises, b.literal(analysis.instance_body.async.length), true)
|
||||||
);
|
);
|
||||||
|
|
@ -1187,15 +1204,20 @@ function calculate_blockers(instance, analysis) {
|
||||||
|
|
||||||
if (node.type === 'ClassDeclaration') {
|
if (node.type === 'ClassDeclaration') {
|
||||||
push_declaration(node.id, blocker);
|
push_declaration(node.id, blocker);
|
||||||
analysis.instance_body.async.push({ node, has_await });
|
}
|
||||||
|
|
||||||
|
if (has_await) {
|
||||||
|
analysis.instance_body.async.push({ nodes: [node], has_await: true });
|
||||||
} else {
|
} else {
|
||||||
analysis.instance_body.async.push({ node, has_await });
|
sync_group.push(node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
analysis.instance_body.sync.push(node);
|
analysis.instance_body.sync.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_sync_group();
|
||||||
|
|
||||||
for (const fn of functions) {
|
for (const fn of functions) {
|
||||||
/** @type {Set<Binding>} */
|
/** @type {Set<Binding>} */
|
||||||
const reads_writes = new Set();
|
const reads_writes = new Set();
|
||||||
|
|
|
||||||
122
frontend/node_modules/svelte/src/compiler/phases/3-transform/shared/transform-async.js
generated
vendored
122
frontend/node_modules/svelte/src/compiler/phases/3-transform/shared/transform-async.js
generated
vendored
|
|
@ -47,64 +47,24 @@ export function transform_body(instance_body, runner, transform) {
|
||||||
|
|
||||||
// Thunks for the await expressions
|
// Thunks for the await expressions
|
||||||
if (instance_body.async.length > 0) {
|
if (instance_body.async.length > 0) {
|
||||||
const thunks = instance_body.async.map((s) => {
|
const thunks = instance_body.async.map((entry) => {
|
||||||
if (s.node.type === 'VariableDeclarator') {
|
/** @type {ESTree.Statement[]} */
|
||||||
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
|
const entry_statements = [];
|
||||||
transform(b.var(s.node.id, s.node.init))
|
|
||||||
);
|
|
||||||
|
|
||||||
const statements =
|
for (const node of entry.nodes) {
|
||||||
visited.type === 'VariableDeclaration'
|
entry_statements.push(...transform_async_node(node, transform));
|
||||||
? visited.declarations.map((node) => {
|
|
||||||
if (
|
|
||||||
node.id.type === 'Identifier' &&
|
|
||||||
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
|
|
||||||
) {
|
|
||||||
// this is an intermediate declaration created in VariableDeclaration.js;
|
|
||||||
// subsequent statements depend on it
|
|
||||||
return b.var(node.id, node.init);
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (statements.length === 1) {
|
|
||||||
const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
|
|
||||||
return b.thunk(statement.expression, s.has_await);
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.thunk(b.block(statements), s.has_await);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.node.type === 'ClassDeclaration') {
|
if (entry_statements.length === 0) {
|
||||||
return b.thunk(
|
// Keep indices stable for async sequencing while avoiding array holes in run([...]).
|
||||||
b.assignment(
|
return b.thunk(b.void0, false);
|
||||||
'=',
|
|
||||||
s.node.id,
|
|
||||||
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
|
|
||||||
),
|
|
||||||
s.has_await
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.node.type === 'ExpressionStatement') {
|
if (entry_statements.length === 1 && entry_statements[0].type === 'ExpressionStatement') {
|
||||||
// the expression may be a $inspect call, which will be transformed into an empty statement
|
return b.thunk(entry_statements[0].expression, entry.has_await);
|
||||||
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
|
|
||||||
transform(s.node.expression)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (expression.type === 'EmptyStatement') {
|
|
||||||
// Keep indices stable for async sequencing while avoiding array holes in run([...]).
|
|
||||||
return b.thunk(b.void0, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return expression.type === 'AwaitExpression'
|
|
||||||
? b.thunk(expression, true)
|
|
||||||
: b.thunk(b.unary('void', expression), s.has_await);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
|
return b.thunk(b.block(entry_statements), entry.has_await);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO get the `$$promises` ID from scope
|
// TODO get the `$$promises` ID from scope
|
||||||
|
|
@ -113,3 +73,63 @@ export function transform_body(instance_body, runner, transform) {
|
||||||
|
|
||||||
return statements;
|
return statements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ESTree.Statement | ESTree.VariableDeclarator} node
|
||||||
|
* @param {(node: ESTree.Node) => ESTree.Node} transform
|
||||||
|
* @returns {ESTree.Statement[]}
|
||||||
|
*/
|
||||||
|
function transform_async_node(node, transform) {
|
||||||
|
if (node.type === 'VariableDeclarator') {
|
||||||
|
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
|
||||||
|
transform(b.var(node.id, node.init))
|
||||||
|
);
|
||||||
|
|
||||||
|
return visited.type === 'VariableDeclaration'
|
||||||
|
? visited.declarations.map((node) => {
|
||||||
|
if (
|
||||||
|
node.id.type === 'Identifier' &&
|
||||||
|
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
|
||||||
|
) {
|
||||||
|
// This intermediate declaration is created in VariableDeclaration.js;
|
||||||
|
// subsequent statements may depend on it.
|
||||||
|
return b.var(node.id, node.init);
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'ClassDeclaration') {
|
||||||
|
return [
|
||||||
|
b.stmt(
|
||||||
|
b.assignment(
|
||||||
|
'=',
|
||||||
|
node.id,
|
||||||
|
/** @type {ESTree.ClassExpression} */ ({ ...node, type: 'ClassExpression' })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'ExpressionStatement') {
|
||||||
|
// The expression may be a $inspect call, which will be transformed into an empty statement.
|
||||||
|
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
|
||||||
|
transform(node.expression)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (expression.type === 'EmptyStatement') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expression.type === 'AwaitExpression') {
|
||||||
|
return [b.stmt(expression)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [b.stmt(b.unary('void', expression))];
|
||||||
|
}
|
||||||
|
|
||||||
|
const statement = /** @type {ESTree.Statement | ESTree.EmptyStatement} */ (transform(node));
|
||||||
|
return statement.type === 'EmptyStatement' ? [] : [statement];
|
||||||
|
}
|
||||||
|
|
|
||||||
9
frontend/node_modules/svelte/src/compiler/print/index.js
generated
vendored
9
frontend/node_modules/svelte/src/compiler/print/index.js
generated
vendored
|
|
@ -592,8 +592,13 @@ const svelte_visitors = (comments) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
ConstTag(node, context) {
|
ConstTag(node, context) {
|
||||||
context.write('{@');
|
context.write('{@const ');
|
||||||
context.visit(node.declaration);
|
const declarators = node.declaration.declarations;
|
||||||
|
for (let i = 0; i < declarators.length; i++) {
|
||||||
|
if (i > 0) context.write(', ');
|
||||||
|
context.visit(declarators[i]);
|
||||||
|
}
|
||||||
|
|
||||||
context.write('}');
|
context.write('}');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
34
frontend/node_modules/svelte/src/internal/client/dev/hmr.js
generated
vendored
34
frontend/node_modules/svelte/src/internal/client/dev/hmr.js
generated
vendored
|
|
@ -5,9 +5,7 @@ import { hydrate_node, hydrating } from '../dom/hydration.js';
|
||||||
import { block, branch, destroy_effect } from '../reactivity/effects.js';
|
import { block, branch, destroy_effect } from '../reactivity/effects.js';
|
||||||
import { set, source } from '../reactivity/sources.js';
|
import { set, source } from '../reactivity/sources.js';
|
||||||
import { set_should_intro } from '../render.js';
|
import { set_should_intro } from '../render.js';
|
||||||
import { get } from '../runtime.js';
|
import { active_effect, get } from '../runtime.js';
|
||||||
import { assign_nodes } from '../dom/template.js';
|
|
||||||
import { create_comment } from '../dom/operations.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template {(anchor: Comment, props: any) => any} Component
|
* @template {(anchor: Comment, props: any) => any} Component
|
||||||
|
|
@ -29,13 +27,6 @@ export function hmr(fn) {
|
||||||
|
|
||||||
let ran = false;
|
let ran = false;
|
||||||
|
|
||||||
// Surround the wrapped effects with comments and assign the nodes
|
|
||||||
// on the wrapping effects so the parent can properly do DOM operations.
|
|
||||||
let start = create_comment();
|
|
||||||
let end = create_comment();
|
|
||||||
|
|
||||||
anchor.before(start);
|
|
||||||
|
|
||||||
block(() => {
|
block(() => {
|
||||||
if (component === (component = get(current))) {
|
if (component === (component = get(current))) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -52,16 +43,21 @@ export function hmr(fn) {
|
||||||
if (ran) set_should_intro(false);
|
if (ran) set_should_intro(false);
|
||||||
|
|
||||||
// preserve getters/setters
|
// preserve getters/setters
|
||||||
Object.defineProperties(
|
var result =
|
||||||
instance,
|
// @ts-expect-error
|
||||||
Object.getOwnPropertyDescriptors(
|
new.target ? new component(anchor, props) : component(anchor, props);
|
||||||
// @ts-expect-error
|
// a component is not guaranteed to return something and we can't invoke getOwnPropertyDescriptors on undefined
|
||||||
new.target ? new component(anchor, props) : component(anchor, props)
|
if (result) {
|
||||||
)
|
Object.defineProperties(instance, Object.getOwnPropertyDescriptors(result));
|
||||||
);
|
}
|
||||||
|
|
||||||
if (ran) set_should_intro(true);
|
if (ran) set_should_intro(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forward the nodes from the inner effect to the outer active effect which would
|
||||||
|
// get them if the HMR wrapper wasn't there. Do this inside the block not outside
|
||||||
|
// so that HMR updates to the component will also update the nodes on the active effect.
|
||||||
|
/** @type {Effect} */ (active_effect).nodes = effect.nodes;
|
||||||
}, EFFECT_TRANSPARENT);
|
}, EFFECT_TRANSPARENT);
|
||||||
|
|
||||||
ran = true;
|
ran = true;
|
||||||
|
|
@ -70,10 +66,6 @@ export function hmr(fn) {
|
||||||
anchor = hydrate_node;
|
anchor = hydrate_node;
|
||||||
}
|
}
|
||||||
|
|
||||||
anchor.before(end);
|
|
||||||
|
|
||||||
assign_nodes(start, end);
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
frontend/node_modules/svelte/src/internal/client/reactivity/async.js
generated
vendored
11
frontend/node_modules/svelte/src/internal/client/reactivity/async.js
generated
vendored
|
|
@ -209,8 +209,8 @@ export async function* for_await_track_reactivity_loss(iterable) {
|
||||||
yield value;
|
yield value;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
|
// If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
|
||||||
if (normal_completion && iterator.return !== undefined) {
|
if (!normal_completion && iterator.return !== undefined) {
|
||||||
// eslint-disable-next-line no-unsafe-finally
|
// eslint-disable-next-line no-unsafe-finally
|
||||||
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
|
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
|
||||||
}
|
}
|
||||||
|
|
@ -307,15 +307,16 @@ export function wait(blockers) {
|
||||||
* @returns {(skip?: boolean) => void}
|
* @returns {(skip?: boolean) => void}
|
||||||
*/
|
*/
|
||||||
export function increment_pending() {
|
export function increment_pending() {
|
||||||
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
|
var effect = /** @type {Effect} */ (active_effect);
|
||||||
|
var boundary = /** @type {Boundary} */ (effect.b);
|
||||||
var batch = /** @type {Batch} */ (current_batch);
|
var batch = /** @type {Batch} */ (current_batch);
|
||||||
var blocking = boundary.is_rendered();
|
var blocking = boundary.is_rendered();
|
||||||
|
|
||||||
boundary.update_pending_count(1, batch);
|
boundary.update_pending_count(1, batch);
|
||||||
batch.increment(blocking);
|
batch.increment(blocking, effect);
|
||||||
|
|
||||||
return (skip = false) => {
|
return (skip = false) => {
|
||||||
boundary.update_pending_count(-1, batch);
|
boundary.update_pending_count(-1, batch);
|
||||||
batch.decrement(blocking, skip);
|
batch.decrement(blocking, effect, skip);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
148
frontend/node_modules/svelte/src/internal/client/reactivity/batch.js
generated
vendored
148
frontend/node_modules/svelte/src/internal/client/reactivity/batch.js
generated
vendored
|
|
@ -90,20 +90,20 @@ var source_stacks = DEV ? new Set() : null;
|
||||||
let uid = 1;
|
let uid = 1;
|
||||||
|
|
||||||
export class Batch {
|
export class Batch {
|
||||||
// for debugging. TODO remove once async is stable
|
|
||||||
id = uid++;
|
id = uid++;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current values of any sources that are updated in this batch
|
* The current values of any signals that are updated in this batch.
|
||||||
|
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
|
||||||
* They keys of this map are identical to `this.#previous`
|
* They keys of this map are identical to `this.#previous`
|
||||||
* @type {Map<Source, any>}
|
* @type {Map<Value, [any, boolean]>}
|
||||||
*/
|
*/
|
||||||
current = new Map();
|
current = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The values of any sources that are updated in this batch _before_ those updates took place.
|
* The values of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place.
|
||||||
* They keys of this map are identical to `this.#current`
|
* They keys of this map are identical to `this.#current`
|
||||||
* @type {Map<Source, any>}
|
* @type {Map<Value, any>}
|
||||||
*/
|
*/
|
||||||
previous = new Map();
|
previous = new Map();
|
||||||
|
|
||||||
|
|
@ -121,14 +121,16 @@ export class Batch {
|
||||||
#discard_callbacks = new Set();
|
#discard_callbacks = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of async effects that are currently in flight
|
* Async effects that are currently in flight
|
||||||
|
* @type {Map<Effect, number>}
|
||||||
*/
|
*/
|
||||||
#pending = 0;
|
#pending = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of async effects that are currently in flight, _not_ inside a pending boundary
|
* Async effects that are currently in flight, _not_ inside a pending boundary
|
||||||
|
* @type {Map<Effect, number>}
|
||||||
*/
|
*/
|
||||||
#blocking_pending = 0;
|
#blocking_pending = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A deferred that resolves when the batch is committed, used with `settled()`
|
* A deferred that resolves when the batch is committed, used with `settled()`
|
||||||
|
|
@ -168,8 +170,35 @@ export class Batch {
|
||||||
|
|
||||||
#decrement_queued = false;
|
#decrement_queued = false;
|
||||||
|
|
||||||
|
/** @type {Set<Batch>} */
|
||||||
|
#blockers = new Set();
|
||||||
|
|
||||||
#is_deferred() {
|
#is_deferred() {
|
||||||
return this.is_fork || this.#blocking_pending > 0;
|
return this.is_fork || this.#blocking_pending.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#is_blocked() {
|
||||||
|
for (const batch of this.#blockers) {
|
||||||
|
for (const effect of batch.#blocking_pending.keys()) {
|
||||||
|
var skipped = false;
|
||||||
|
var e = effect;
|
||||||
|
|
||||||
|
while (e.parent !== null) {
|
||||||
|
if (this.#skipped_branches.has(e)) {
|
||||||
|
skipped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
e = e.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -264,7 +293,7 @@ export class Batch {
|
||||||
collected_effects = null;
|
collected_effects = null;
|
||||||
legacy_updates = null;
|
legacy_updates = null;
|
||||||
|
|
||||||
if (this.#is_deferred()) {
|
if (this.#is_deferred() || this.#is_blocked()) {
|
||||||
this.#defer_effects(render_effects);
|
this.#defer_effects(render_effects);
|
||||||
this.#defer_effects(effects);
|
this.#defer_effects(effects);
|
||||||
|
|
||||||
|
|
@ -272,7 +301,7 @@ export class Batch {
|
||||||
reset_branch(e, t);
|
reset_branch(e, t);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.#pending === 0) {
|
if (this.#pending.size === 0) {
|
||||||
batches.delete(this);
|
batches.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,17 +412,18 @@ export class Batch {
|
||||||
/**
|
/**
|
||||||
* Associate a change to a given source with the current
|
* Associate a change to a given source with the current
|
||||||
* batch, noting its previous and current values
|
* batch, noting its previous and current values
|
||||||
* @param {Source} source
|
* @param {Value} source
|
||||||
* @param {any} old_value
|
* @param {any} old_value
|
||||||
|
* @param {boolean} [is_derived]
|
||||||
*/
|
*/
|
||||||
capture(source, old_value) {
|
capture(source, old_value, is_derived = false) {
|
||||||
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
|
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
|
||||||
this.previous.set(source, old_value);
|
this.previous.set(source, old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
|
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
|
||||||
if ((source.f & ERROR_VALUE) === 0) {
|
if ((source.f & ERROR_VALUE) === 0) {
|
||||||
this.current.set(source, source.v);
|
this.current.set(source, [source.v, is_derived]);
|
||||||
batch_values?.set(source, source.v);
|
batch_values?.set(source, source.v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -453,11 +483,13 @@ export class Batch {
|
||||||
/** @type {Source[]} */
|
/** @type {Source[]} */
|
||||||
var sources = [];
|
var sources = [];
|
||||||
|
|
||||||
for (const [source, value] of this.current) {
|
for (const [source, [value, is_derived]] of this.current) {
|
||||||
if (batch.current.has(source)) {
|
if (batch.current.has(source)) {
|
||||||
if (is_earlier && value !== batch.current.get(source)) {
|
var batch_value = /** @type {[any, boolean]} */ (batch.current.get(source))[0]; // faster than destructuring
|
||||||
|
|
||||||
|
if (is_earlier && value !== batch_value) {
|
||||||
// bring the value up to date
|
// bring the value up to date
|
||||||
batch.current.set(source, value);
|
batch.current.set(source, [value, is_derived]);
|
||||||
} else {
|
} else {
|
||||||
// same value or later batch has more recent value,
|
// same value or later batch has more recent value,
|
||||||
// no need to re-run these effects
|
// no need to re-run these effects
|
||||||
|
|
@ -507,24 +539,56 @@ export class Batch {
|
||||||
batch.deactivate();
|
batch.deactivate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
if (batch.#blockers.has(this)) {
|
||||||
|
batch.#blockers.delete(this);
|
||||||
|
|
||||||
|
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
|
||||||
|
batch.activate();
|
||||||
|
batch.#process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {boolean} blocking
|
* @param {boolean} blocking
|
||||||
|
* @param {Effect} effect
|
||||||
*/
|
*/
|
||||||
increment(blocking) {
|
increment(blocking, effect) {
|
||||||
this.#pending += 1;
|
let pending_count = this.#pending.get(effect) ?? 0;
|
||||||
if (blocking) this.#blocking_pending += 1;
|
this.#pending.set(effect, pending_count + 1);
|
||||||
|
|
||||||
|
if (blocking) {
|
||||||
|
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
|
||||||
|
this.#blocking_pending.set(effect, blocking_pending_count + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} blocking
|
* @param {boolean} blocking
|
||||||
|
* @param {Effect} effect
|
||||||
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
|
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
|
||||||
*/
|
*/
|
||||||
decrement(blocking, skip) {
|
decrement(blocking, effect, skip) {
|
||||||
this.#pending -= 1;
|
let pending_count = this.#pending.get(effect) ?? 0;
|
||||||
if (blocking) this.#blocking_pending -= 1;
|
|
||||||
|
if (pending_count === 1) {
|
||||||
|
this.#pending.delete(effect);
|
||||||
|
} else {
|
||||||
|
this.#pending.set(effect, pending_count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocking) {
|
||||||
|
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
|
||||||
|
|
||||||
|
if (blocking_pending_count === 1) {
|
||||||
|
this.#blocking_pending.delete(effect);
|
||||||
|
} else {
|
||||||
|
this.#blocking_pending.set(effect, blocking_pending_count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.#decrement_queued || skip) return;
|
if (this.#decrement_queued || skip) return;
|
||||||
this.#decrement_queued = true;
|
this.#decrement_queued = true;
|
||||||
|
|
@ -597,15 +661,37 @@ export class Batch {
|
||||||
|
|
||||||
// if there are multiple batches, we are 'time travelling' —
|
// if there are multiple batches, we are 'time travelling' —
|
||||||
// we need to override values with the ones in this batch...
|
// we need to override values with the ones in this batch...
|
||||||
batch_values = new Map(this.current);
|
batch_values = new Map();
|
||||||
|
for (const [source, [value]] of this.current) {
|
||||||
|
batch_values.set(source, value);
|
||||||
|
}
|
||||||
|
|
||||||
// ...and undo changes belonging to other batches
|
// ...and undo changes belonging to other batches unless they block this one
|
||||||
for (const batch of batches) {
|
for (const batch of batches) {
|
||||||
if (batch === this || batch.is_fork) continue;
|
if (batch === this || batch.is_fork) continue;
|
||||||
|
|
||||||
for (const [source, previous] of batch.previous) {
|
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
|
||||||
if (!batch_values.has(source)) {
|
var intersects = false;
|
||||||
batch_values.set(source, previous);
|
var differs = false;
|
||||||
|
|
||||||
|
if (batch.id < this.id) {
|
||||||
|
for (const [source, [, is_derived]] of batch.current) {
|
||||||
|
// Derived values don't partake in the blocking mechanism, because a derived could
|
||||||
|
// be triggered in one batch already but not the other one yet, causing a false-positive
|
||||||
|
if (is_derived) continue;
|
||||||
|
|
||||||
|
intersects ||= this.current.has(source);
|
||||||
|
differs ||= !this.current.has(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersects && differs) {
|
||||||
|
this.#blockers.add(batch);
|
||||||
|
} else {
|
||||||
|
for (const [source, previous] of batch.previous) {
|
||||||
|
if (!batch_values.has(source)) {
|
||||||
|
batch_values.set(source, previous);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1065,7 +1151,7 @@ export function fork(fn) {
|
||||||
batch.is_fork = false;
|
batch.is_fork = false;
|
||||||
|
|
||||||
// apply changes and update write versions so deriveds see the change
|
// apply changes and update write versions so deriveds see the change
|
||||||
for (var [source, value] of batch.current) {
|
for (var [source, [value]] of batch.current) {
|
||||||
source.v = value;
|
source.v = value;
|
||||||
source.wv = increment_write_version();
|
source.wv = increment_write_version();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
frontend/node_modules/svelte/src/internal/client/reactivity/deriveds.js
generated
vendored
2
frontend/node_modules/svelte/src/internal/client/reactivity/deriveds.js
generated
vendored
|
|
@ -396,7 +396,7 @@ export function update_derived(derived) {
|
||||||
// change, `derived.equals` may incorrectly return `true`
|
// change, `derived.equals` may incorrectly return `true`
|
||||||
if (!current_batch?.is_fork || derived.deps === null) {
|
if (!current_batch?.is_fork || derived.deps === null) {
|
||||||
derived.v = value;
|
derived.v = value;
|
||||||
current_batch?.capture(derived, old_value);
|
current_batch?.capture(derived, old_value, true);
|
||||||
|
|
||||||
// deriveds without dependencies should never be recomputed
|
// deriveds without dependencies should never be recomputed
|
||||||
if (derived.deps === null) {
|
if (derived.deps === null) {
|
||||||
|
|
|
||||||
1
frontend/node_modules/svelte/src/internal/client/reactivity/effects.js
generated
vendored
1
frontend/node_modules/svelte/src/internal/client/reactivity/effects.js
generated
vendored
|
|
@ -559,6 +559,7 @@ export function destroy_effect(effect, remove_dom = true) {
|
||||||
effect.fn =
|
effect.fn =
|
||||||
effect.nodes =
|
effect.nodes =
|
||||||
effect.ac =
|
effect.ac =
|
||||||
|
effect.b =
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
14
frontend/node_modules/svelte/src/motion/spring.js
generated
vendored
14
frontend/node_modules/svelte/src/motion/spring.js
generated
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
/** @import { Task } from '#client' */
|
/** @import { Task } from '#client' */
|
||||||
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
|
/** @import { TickContext } from './private.js' */
|
||||||
/** @import { Spring as SpringStore } from './public.js' */
|
/** @import { Spring as SpringStore, SpringOptions, SpringUpdateOptions } from './public.js' */
|
||||||
import { writable } from '../store/shared/index.js';
|
import { writable } from '../store/shared/index.js';
|
||||||
import { loop } from '../internal/client/loop.js';
|
import { loop } from '../internal/client/loop.js';
|
||||||
import { raf } from '../internal/client/timing.js';
|
import { raf } from '../internal/client/timing.js';
|
||||||
|
|
@ -62,7 +62,7 @@ function tick_spring(ctx, last_value, current_value, target_value) {
|
||||||
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
|
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
|
||||||
* @template [T=any]
|
* @template [T=any]
|
||||||
* @param {T} [value]
|
* @param {T} [value]
|
||||||
* @param {SpringOpts} [opts]
|
* @param {SpringOptions} [opts]
|
||||||
* @returns {SpringStore<T>}
|
* @returns {SpringStore<T>}
|
||||||
*/
|
*/
|
||||||
export function spring(value, opts = {}) {
|
export function spring(value, opts = {}) {
|
||||||
|
|
@ -83,7 +83,7 @@ export function spring(value, opts = {}) {
|
||||||
let cancel_task = false;
|
let cancel_task = false;
|
||||||
/**
|
/**
|
||||||
* @param {T} new_value
|
* @param {T} new_value
|
||||||
* @param {SpringUpdateOpts} opts
|
* @param {SpringUpdateOptions} opts
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
function set(new_value, opts = {}) {
|
function set(new_value, opts = {}) {
|
||||||
|
|
@ -191,7 +191,7 @@ export class Spring {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {T} value
|
* @param {T} value
|
||||||
* @param {SpringOpts} [options]
|
* @param {SpringOptions} [options]
|
||||||
*/
|
*/
|
||||||
constructor(value, options = {}) {
|
constructor(value, options = {}) {
|
||||||
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
|
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
|
||||||
|
|
@ -225,7 +225,7 @@ export class Spring {
|
||||||
* ```
|
* ```
|
||||||
* @template U
|
* @template U
|
||||||
* @param {() => U} fn
|
* @param {() => U} fn
|
||||||
* @param {SpringOpts} [options]
|
* @param {SpringOptions} [options]
|
||||||
*/
|
*/
|
||||||
static of(fn, options) {
|
static of(fn, options) {
|
||||||
const spring = new Spring(fn(), options);
|
const spring = new Spring(fn(), options);
|
||||||
|
|
@ -293,7 +293,7 @@ export class Spring {
|
||||||
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
|
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
|
||||||
*
|
*
|
||||||
* @param {T} value
|
* @param {T} value
|
||||||
* @param {SpringUpdateOpts} [options]
|
* @param {SpringUpdateOptions} [options]
|
||||||
*/
|
*/
|
||||||
set(value, options) {
|
set(value, options) {
|
||||||
this.#deferred?.reject(new Error('Aborted'));
|
this.#deferred?.reject(new Error('Aborted'));
|
||||||
|
|
|
||||||
15
frontend/node_modules/svelte/src/motion/tweened.js
generated
vendored
15
frontend/node_modules/svelte/src/motion/tweened.js
generated
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
/** @import { Task } from '../internal/client/types' */
|
/** @import { Task } from '../internal/client/types' */
|
||||||
/** @import { Tweened } from './public' */
|
/** @import { Tweened, TweenOptions } from './public' */
|
||||||
/** @import { TweenedOptions } from './private' */
|
|
||||||
import { writable } from '../store/shared/index.js';
|
import { writable } from '../store/shared/index.js';
|
||||||
import { raf } from '../internal/client/timing.js';
|
import { raf } from '../internal/client/timing.js';
|
||||||
import { loop } from '../internal/client/loop.js';
|
import { loop } from '../internal/client/loop.js';
|
||||||
|
|
@ -84,7 +83,7 @@ function get_interpolator(a, b) {
|
||||||
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
|
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
|
||||||
* @template T
|
* @template T
|
||||||
* @param {T} [value]
|
* @param {T} [value]
|
||||||
* @param {TweenedOptions<T>} [defaults]
|
* @param {TweenOptions<T>} [defaults]
|
||||||
* @returns {Tweened<T>}
|
* @returns {Tweened<T>}
|
||||||
*/
|
*/
|
||||||
export function tweened(value, defaults = {}) {
|
export function tweened(value, defaults = {}) {
|
||||||
|
|
@ -94,7 +93,7 @@ export function tweened(value, defaults = {}) {
|
||||||
let target_value = value;
|
let target_value = value;
|
||||||
/**
|
/**
|
||||||
* @param {T} new_value
|
* @param {T} new_value
|
||||||
* @param {TweenedOptions<T>} [opts]
|
* @param {TweenOptions<T>} [opts]
|
||||||
*/
|
*/
|
||||||
function set(new_value, opts) {
|
function set(new_value, opts) {
|
||||||
target_value = new_value;
|
target_value = new_value;
|
||||||
|
|
@ -180,7 +179,7 @@ export class Tween {
|
||||||
#current;
|
#current;
|
||||||
#target;
|
#target;
|
||||||
|
|
||||||
/** @type {TweenedOptions<T>} */
|
/** @type {TweenOptions<T>} */
|
||||||
#defaults;
|
#defaults;
|
||||||
|
|
||||||
/** @type {import('../internal/client/types').Task | null} */
|
/** @type {import('../internal/client/types').Task | null} */
|
||||||
|
|
@ -188,7 +187,7 @@ export class Tween {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {T} value
|
* @param {T} value
|
||||||
* @param {TweenedOptions<T>} options
|
* @param {TweenOptions<T>} options
|
||||||
*/
|
*/
|
||||||
constructor(value, options = {}) {
|
constructor(value, options = {}) {
|
||||||
this.#current = state(value);
|
this.#current = state(value);
|
||||||
|
|
@ -216,7 +215,7 @@ export class Tween {
|
||||||
* ```
|
* ```
|
||||||
* @template U
|
* @template U
|
||||||
* @param {() => U} fn
|
* @param {() => U} fn
|
||||||
* @param {TweenedOptions<U>} [options]
|
* @param {TweenOptions<U>} [options]
|
||||||
*/
|
*/
|
||||||
static of(fn, options) {
|
static of(fn, options) {
|
||||||
const tween = new Tween(fn(), options);
|
const tween = new Tween(fn(), options);
|
||||||
|
|
@ -233,7 +232,7 @@ export class Tween {
|
||||||
*
|
*
|
||||||
* If `options` are provided, they will override the tween's defaults.
|
* If `options` are provided, they will override the tween's defaults.
|
||||||
* @param {T} value
|
* @param {T} value
|
||||||
* @param {TweenedOptions<T>} [options]
|
* @param {TweenOptions<T>} [options]
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
set(value, options) {
|
set(value, options) {
|
||||||
|
|
|
||||||
2
frontend/node_modules/svelte/src/version.js
generated
vendored
2
frontend/node_modules/svelte/src/version.js
generated
vendored
|
|
@ -4,5 +4,5 @@
|
||||||
* The current version, as set in package.json.
|
* The current version, as set in package.json.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
export const VERSION = '5.54.0';
|
export const VERSION = '5.55.0';
|
||||||
export const PUBLIC_VERSION = '5';
|
export const PUBLIC_VERSION = '5';
|
||||||
|
|
|
||||||
91
frontend/node_modules/svelte/types/index.d.ts
generated
vendored
91
frontend/node_modules/svelte/types/index.d.ts
generated
vendored
|
|
@ -2005,16 +2005,50 @@ declare module 'svelte/legacy' {
|
||||||
|
|
||||||
declare module 'svelte/motion' {
|
declare module 'svelte/motion' {
|
||||||
import type { MediaQuery } from 'svelte/reactivity';
|
import type { MediaQuery } from 'svelte/reactivity';
|
||||||
|
export interface SpringOptions {
|
||||||
|
stiffness?: number;
|
||||||
|
damping?: number;
|
||||||
|
precision?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpringUpdateOptions {
|
||||||
|
/**
|
||||||
|
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
|
||||||
|
*/
|
||||||
|
hard?: any;
|
||||||
|
/**
|
||||||
|
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
|
||||||
|
*/
|
||||||
|
soft?: string | number | boolean;
|
||||||
|
/**
|
||||||
|
* Only use this for the Spring class; does nothing when set on the spring store
|
||||||
|
*/
|
||||||
|
instant?: boolean;
|
||||||
|
/**
|
||||||
|
* Only use this for the Spring class; does nothing when set on the spring store
|
||||||
|
*/
|
||||||
|
preserveMomentum?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Updater<T> = (target_value: T, value: T) => T;
|
||||||
|
|
||||||
|
export interface TweenOptions<T> {
|
||||||
|
delay?: number;
|
||||||
|
duration?: number | ((from: T, to: T) => number);
|
||||||
|
easing?: (t: number) => number;
|
||||||
|
interpolate?: (a: T, b: T) => (t: number) => T;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
|
// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
|
||||||
// this means both the Spring class and the Spring interface are merged into one with some things only
|
// this means both the Spring class and the Spring interface are merged into one with some things only
|
||||||
// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
|
// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
|
||||||
|
|
||||||
export interface Spring<T> extends Readable<T> {
|
export interface Spring<T> extends Readable<T> {
|
||||||
set(new_value: T, opts?: SpringUpdateOpts): Promise<void>;
|
set(new_value: T, opts?: SpringUpdateOptions): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
|
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
|
||||||
*/
|
*/
|
||||||
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
|
update: (fn: Updater<T>, opts?: SpringUpdateOptions) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
|
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
|
||||||
*/
|
*/
|
||||||
|
|
@ -2041,7 +2075,7 @@ declare module 'svelte/motion' {
|
||||||
* @since 5.8.0
|
* @since 5.8.0
|
||||||
*/
|
*/
|
||||||
export class Spring<T> {
|
export class Spring<T> {
|
||||||
constructor(value: T, options?: SpringOpts);
|
constructor(value: T, options?: SpringOptions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a spring whose value is bound to the return value of `fn`. This must be called
|
* Create a spring whose value is bound to the return value of `fn`. This must be called
|
||||||
|
|
@ -2057,7 +2091,7 @@ declare module 'svelte/motion' {
|
||||||
* </script>
|
* </script>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static of<U>(fn: () => U, options?: SpringOpts): Spring<U>;
|
static of<U>(fn: () => U, options?: SpringOptions): Spring<U>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
|
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
|
||||||
|
|
@ -2067,7 +2101,7 @@ declare module 'svelte/motion' {
|
||||||
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
|
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
|
||||||
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
|
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
|
||||||
*/
|
*/
|
||||||
set(value: T, options?: SpringUpdateOpts): Promise<void>;
|
set(value: T, options?: SpringUpdateOptions): Promise<void>;
|
||||||
|
|
||||||
damping: number;
|
damping: number;
|
||||||
precision: number;
|
precision: number;
|
||||||
|
|
@ -2085,8 +2119,8 @@ declare module 'svelte/motion' {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tweened<T> extends Readable<T> {
|
export interface Tweened<T> extends Readable<T> {
|
||||||
set(value: T, opts?: TweenedOptions<T>): Promise<void>;
|
set(value: T, opts?: TweenOptions<T>): Promise<void>;
|
||||||
update(updater: Updater<T>, opts?: TweenedOptions<T>): Promise<void>;
|
update(updater: Updater<T>, opts?: TweenOptions<T>): Promise<void>;
|
||||||
}
|
}
|
||||||
/** Callback to inform of a value updates. */
|
/** Callback to inform of a value updates. */
|
||||||
type Subscriber<T> = (value: T) => void;
|
type Subscriber<T> = (value: T) => void;
|
||||||
|
|
@ -2103,39 +2137,6 @@ declare module 'svelte/motion' {
|
||||||
*/
|
*/
|
||||||
subscribe(this: void, run: Subscriber<T>, invalidate?: () => void): Unsubscriber;
|
subscribe(this: void, run: Subscriber<T>, invalidate?: () => void): Unsubscriber;
|
||||||
}
|
}
|
||||||
interface SpringOpts {
|
|
||||||
stiffness?: number;
|
|
||||||
damping?: number;
|
|
||||||
precision?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpringUpdateOpts {
|
|
||||||
/**
|
|
||||||
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
|
|
||||||
*/
|
|
||||||
hard?: any;
|
|
||||||
/**
|
|
||||||
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
|
|
||||||
*/
|
|
||||||
soft?: string | number | boolean;
|
|
||||||
/**
|
|
||||||
* Only use this for the Spring class; does nothing when set on the spring store
|
|
||||||
*/
|
|
||||||
instant?: boolean;
|
|
||||||
/**
|
|
||||||
* Only use this for the Spring class; does nothing when set on the spring store
|
|
||||||
*/
|
|
||||||
preserveMomentum?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Updater<T> = (target_value: T, value: T) => T;
|
|
||||||
|
|
||||||
interface TweenedOptions<T> {
|
|
||||||
delay?: number;
|
|
||||||
duration?: number | ((from: T, to: T) => number);
|
|
||||||
easing?: (t: number) => number;
|
|
||||||
interpolate?: (a: T, b: T) => (t: number) => T;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
|
* A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
|
||||||
*
|
*
|
||||||
|
|
@ -2165,13 +2166,13 @@ declare module 'svelte/motion' {
|
||||||
*
|
*
|
||||||
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
|
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
|
||||||
* */
|
* */
|
||||||
export function spring<T = any>(value?: T | undefined, opts?: SpringOpts | undefined): Spring<T>;
|
export function spring<T = any>(value?: T | undefined, opts?: SpringOptions | undefined): Spring<T>;
|
||||||
/**
|
/**
|
||||||
* A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time.
|
* A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time.
|
||||||
*
|
*
|
||||||
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
|
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
|
||||||
* */
|
* */
|
||||||
export function tweened<T>(value?: T | undefined, defaults?: TweenedOptions<T> | undefined): Tweened<T>;
|
export function tweened<T>(value?: T | undefined, defaults?: TweenOptions<T> | undefined): Tweened<T>;
|
||||||
/**
|
/**
|
||||||
* A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
|
* A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
|
||||||
* move towards it over time, taking account of the `delay`, `duration` and `easing` options.
|
* move towards it over time, taking account of the `delay`, `duration` and `easing` options.
|
||||||
|
|
@ -2204,15 +2205,15 @@ declare module 'svelte/motion' {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
static of<U>(fn: () => U, options?: TweenedOptions<U> | undefined): Tween<U>;
|
static of<U>(fn: () => U, options?: TweenOptions<U> | undefined): Tween<U>;
|
||||||
|
|
||||||
constructor(value: T, options?: TweenedOptions<T>);
|
constructor(value: T, options?: TweenOptions<T>);
|
||||||
/**
|
/**
|
||||||
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
|
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
|
||||||
*
|
*
|
||||||
* If `options` are provided, they will override the tween's defaults.
|
* If `options` are provided, they will override the tween's defaults.
|
||||||
* */
|
* */
|
||||||
set(value: T, options?: TweenedOptions<T> | undefined): Promise<void>;
|
set(value: T, options?: TweenOptions<T> | undefined): Promise<void>;
|
||||||
get current(): T;
|
get current(): T;
|
||||||
set target(v: T);
|
set target(v: T);
|
||||||
get target(): T;
|
get target(): T;
|
||||||
|
|
|
||||||
12
frontend/node_modules/svelte/types/index.d.ts.map
generated
vendored
12
frontend/node_modules/svelte/types/index.d.ts.map
generated
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "imc-vibe-frontend",
|
"name": "imc-frontend",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-heros": "^8.0.1",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { decodeJWT, isTokenValid } from '$lib/auth';
|
import { decodeJWT, isTokenValid } from '$lib/auth';
|
||||||
|
import DocumentText from 'svelte-heros/DocumentText.svelte';
|
||||||
|
import GlobeAlt from 'svelte-heros/GlobeAlt.svelte';
|
||||||
|
import Home from 'svelte-heros/Home.svelte';
|
||||||
|
import Mail from 'svelte-heros/Mail.svelte';
|
||||||
|
import MenuAlt4 from 'svelte-heros/MenuAlt4.svelte';
|
||||||
|
import Refresh from 'svelte-heros/Refresh.svelte';
|
||||||
|
import Users from 'svelte-heros/Users.svelte';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
@ -31,9 +38,9 @@
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const path = $page.url.pathname;
|
const path = $page.url.pathname;
|
||||||
// Auth-only routes (login, forgot) - no auth required, no layout chrome
|
// Auth-only routes (login) - no auth required, no layout chrome
|
||||||
// /auth/change-password requires auth, so treat it like a regular route
|
// /auth/change-password requires auth, so treat it like a regular route
|
||||||
const isAuthOnlyRoute = path === '/auth/login' || path === '/auth/forgot';
|
const isAuthOnlyRoute = path === '/auth/login';
|
||||||
if (isAuthOnlyRoute) {
|
if (isAuthOnlyRoute) {
|
||||||
loading = false;
|
loading = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -84,16 +91,20 @@
|
||||||
<header class="navbar bg-base-100 shadow-sm">
|
<header class="navbar bg-base-100 shadow-sm">
|
||||||
<div class="flex-none lg:hidden">
|
<div class="flex-none lg:hidden">
|
||||||
<label for="nav-drawer" class="btn btn-square btn-ghost">
|
<label for="nav-drawer" class="btn btn-square btn-ghost">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<MenuAlt4 class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
|
||||||
</svg>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<a href="/" class="btn btn-ghost text-xl">IMC Vibe</a>
|
<a href="/" class="btn btn-ghost text-xl">IMC</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if selectedDomain}
|
||||||
|
<div class="flex-1 text-center">
|
||||||
|
<span class="text-lg font-semibold opacity-80">{decodeURIComponent(selectedDomain)}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
||||||
|
|
@ -127,41 +138,29 @@
|
||||||
<span class="text-sm font-semibold uppercase opacity-60">Navigation</span>
|
<span class="text-sm font-semibold uppercase opacity-60">Navigation</span>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/" class="flex items-center gap-3">
|
<li><a href="/" class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Home class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
||||||
</svg>
|
|
||||||
Dashboard
|
Dashboard
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="/domains" class="flex items-center gap-3">
|
<li><a href="/domains" class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<GlobeAlt class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
||||||
</svg>
|
|
||||||
Domains
|
Domains
|
||||||
</a></li>
|
</a></li>
|
||||||
{#if selectedDomain}
|
{#if selectedDomain}
|
||||||
<li><a href={usersLink} class="flex items-center gap-3">
|
<li><a href={usersLink} class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Users class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>
|
|
||||||
Users
|
Users
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href={aliasesLink} class="flex items-center gap-3">
|
<li><a href={aliasesLink} class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Mail class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
Aliases
|
Aliases
|
||||||
</a></li>
|
</a></li>
|
||||||
{/if}
|
{/if}
|
||||||
<li><a href="/queue" class="flex items-center gap-3">
|
<li><a href="/queue" class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Refresh class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
|
||||||
Mail Queue
|
Mail Queue
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="/logs" class="flex items-center gap-3">
|
<li><a href="/logs" class="flex items-center gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<DocumentText class="h-5 w-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
Logs
|
Logs
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import CheckCircle from 'svelte-heros/CheckCircle.svelte';
|
||||||
|
|
||||||
let oldPassword = $state('');
|
let oldPassword = $state('');
|
||||||
let newPassword = $state('');
|
let newPassword = $state('');
|
||||||
let confirmPassword = $state('');
|
let confirmPassword = $state('');
|
||||||
|
|
@ -64,9 +66,7 @@
|
||||||
|
|
||||||
{#if success}
|
{#if success}
|
||||||
<div class="alert alert-success mb-4">
|
<div class="alert alert-success mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
<CheckCircle class="h-6 w-6" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>Password changed successfully!</span>
|
<span>Password changed successfully!</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
let identifier = $state('');
|
|
||||||
let error = $state('');
|
|
||||||
let success = $state(false);
|
|
||||||
let loading = $state(false);
|
|
||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
error = '';
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/auth/forgot', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ identifier }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
error = data.error || 'Request failed';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
success = true;
|
|
||||||
} catch (err) {
|
|
||||||
error = 'Connection error';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="min-h-screen flex items-center justify-center bg-base-200" data-theme="light">
|
|
||||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
|
||||||
<div class="card-body">
|
|
||||||
<h1 class="text-2xl font-bold text-center mb-4">Reset Password</h1>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div class="alert alert-error mb-4">
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if success}
|
|
||||||
<div class="alert alert-success mb-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>If an account exists with that email or username, a password reset link has been sent.</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center mt-4">
|
|
||||||
<a href="/auth/login" class="btn btn-primary">Back to Login</a>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<p class="text-base-content/60 text-center mb-6">Enter your email address or username and we'll send you a link to reset your password.</p>
|
|
||||||
|
|
||||||
<form onsubmit={handleSubmit} class="space-y-4">
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">Email or Username</legend>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
bind:value={identifier}
|
|
||||||
placeholder="user@example.org or username"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
|
||||||
{#if loading}
|
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
|
||||||
Sending...
|
|
||||||
{:else}
|
|
||||||
Send Reset Link
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
|
||||||
<a href="/auth/login" class="link link-primary">Back to Login</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<div class="min-h-screen flex items-center justify-center bg-base-200" data-theme="light">
|
<div class="min-h-screen flex items-center justify-center bg-base-200" data-theme="light">
|
||||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="text-2xl font-bold text-center mb-2">IMC Vibe</h1>
|
<h1 class="text-2xl font-bold text-center mb-2">IMC</h1>
|
||||||
<p class="text-center text-base-content/60 mb-6">Mail Server Administration</p>
|
<p class="text-center text-base-content/60 mb-6">Mail Server Administration</p>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
|
|
||||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">Email or Username</legend>
|
<legend class="fieldset-legend">Username</legend>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
placeholder="user@example.org or username"
|
placeholder="user@example.org or username"
|
||||||
required
|
required
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
|
autofocus
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
@ -76,10 +77,6 @@
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="text-right">
|
|
||||||
<a href="/auth/forgot" class="link link-primary text-sm">Forgot password?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { decodeJWT } from '$lib/auth';
|
import { decodeJWT } from '$lib/auth';
|
||||||
|
import Plus from 'svelte-heros/Plus.svelte';
|
||||||
|
|
||||||
interface Domain {
|
interface Domain {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -92,9 +93,7 @@
|
||||||
<h2 class="text-2xl font-bold">Domains</h2>
|
<h2 class="text-2xl font-bold">Domains</h2>
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<button class="btn btn-primary" onclick={openModal}>
|
<button class="btn btn-primary" onclick={openModal}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<Plus class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Add Domain
|
Add Domain
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ArrowLeft from 'svelte-heros/ArrowLeft.svelte';
|
||||||
|
|
||||||
interface DomainDetail {
|
interface DomainDetail {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -40,9 +41,7 @@
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/domains" class="btn btn-ghost btn-sm">
|
<a href="/domains" class="btn btn-ghost btn-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<ArrowLeft class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Back to Domains
|
Back to Domains
|
||||||
</a>
|
</a>
|
||||||
{#if domain}
|
{#if domain}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ArrowLeft from 'svelte-heros/ArrowLeft.svelte';
|
||||||
|
import Plus from 'svelte-heros/Plus.svelte';
|
||||||
|
|
||||||
interface Alias {
|
interface Alias {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -96,17 +98,13 @@
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<ArrowLeft class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Back
|
Back
|
||||||
</a>
|
</a>
|
||||||
<h2 class="text-2xl font-bold">Aliases - {domainName}</h2>
|
<h2 class="text-2xl font-bold">Aliases - {domainName}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openModal}>
|
<button class="btn btn-primary" onclick={openModal}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<Plus class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Add Alias
|
Add Alias
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ArrowLeft from 'svelte-heros/ArrowLeft.svelte';
|
||||||
|
import Eye from 'svelte-heros/Eye.svelte';
|
||||||
|
import EyeOff from 'svelte-heros/EyeOff.svelte';
|
||||||
|
import Plus from 'svelte-heros/Plus.svelte';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -15,6 +19,7 @@
|
||||||
let newUser = $state({ password: '', quota: 0 });
|
let newUser = $state({ password: '', quota: 0 });
|
||||||
let createError = $state('');
|
let createError = $state('');
|
||||||
let domainName = $state('');
|
let domainName = $state('');
|
||||||
|
let showPassword = $state(false);
|
||||||
|
|
||||||
async function loadUsers(name: string) {
|
async function loadUsers(name: string) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -36,7 +41,8 @@
|
||||||
function openModal() {
|
function openModal() {
|
||||||
createError = '';
|
createError = '';
|
||||||
localPart = '';
|
localPart = '';
|
||||||
newUser = { password: '', quota: 0 };
|
showPassword = false;
|
||||||
|
newUser = { password: generatePasswordStr(), quota: 0 };
|
||||||
dialogEl?.showModal();
|
dialogEl?.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +50,17 @@
|
||||||
dialogEl?.close();
|
dialogEl?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generatePasswordStr(): string {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||||
|
let password = '';
|
||||||
|
const array = new Uint8Array(16);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
password += chars[array[i] % chars.length];
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
async function createUser() {
|
async function createUser() {
|
||||||
createError = '';
|
createError = '';
|
||||||
const email = `${localPart}@${domainName}`;
|
const email = `${localPart}@${domainName}`;
|
||||||
|
|
@ -58,11 +75,11 @@
|
||||||
body: JSON.stringify({ email, password: newUser.password, quota: newUser.quota })
|
body: JSON.stringify({ email, password: newUser.password, quota: newUser.quota })
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
closeModal();
|
|
||||||
await loadUsers(domainName);
|
await loadUsers(domainName);
|
||||||
|
closeModal();
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const data = await res.json();
|
||||||
createError = error.error || 'Failed to create user';
|
createError = data.error || 'Failed to create user';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
createError = 'Failed to create user';
|
createError = 'Failed to create user';
|
||||||
|
|
@ -70,6 +87,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyPassword() {
|
||||||
|
await navigator.clipboard.writeText(newUser.password);
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser(id: number) {
|
async function deleteUser(id: number) {
|
||||||
if (!confirm('Delete this user? Mailbox will NOT be deleted.')) return;
|
if (!confirm('Delete this user? Mailbox will NOT be deleted.')) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -118,17 +139,13 @@
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
<a href="/domains/{domainName}" class="btn btn-ghost btn-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<ArrowLeft class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Back
|
Back
|
||||||
</a>
|
</a>
|
||||||
<h2 class="text-2xl font-bold">Users - {domainName}</h2>
|
<h2 class="text-2xl font-bold">Users - {domainName}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openModal}>
|
<button class="btn btn-primary" onclick={openModal}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<Plus class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Add User
|
Add User
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,12 +209,27 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset mt-4">
|
<fieldset class="fieldset mt-4">
|
||||||
<legend class="fieldset-legend">Password</legend>
|
<legend class="fieldset-legend">Password</legend>
|
||||||
<input
|
<div class="join w-full">
|
||||||
type="password"
|
<input
|
||||||
class="input input-bordered w-full"
|
type={showPassword ? 'text' : 'password'}
|
||||||
bind:value={newUser.password}
|
class="input input-bordered join-item flex-1 font-mono"
|
||||||
required
|
bind:value={newUser.password}
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
|
<button type="button" class="btn join-item" onclick={() => showPassword = !showPassword}>
|
||||||
|
{#if showPassword}
|
||||||
|
<EyeOff class="h-5 w-5" />
|
||||||
|
{:else}
|
||||||
|
<Eye class="h-5 w-5" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn join-item" onclick={() => navigator.clipboard.writeText(newUser.password)}>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary join-item" onclick={() => newUser.password = generatePasswordStr()}>
|
||||||
|
Regenerate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset mt-4">
|
<fieldset class="fieldset mt-4">
|
||||||
<legend class="fieldset-legend">Quota (bytes, 0 = default)</legend>
|
<legend class="fieldset-legend">Quota (bytes, 0 = default)</legend>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Refresh from 'svelte-heros/Refresh.svelte';
|
||||||
|
|
||||||
interface QueueEntry {
|
interface QueueEntry {
|
||||||
id: string;
|
id: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
|
@ -74,9 +76,7 @@
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="text-2xl font-bold">Mail Queue</h2>
|
<h2 class="text-2xl font-bold">Mail Queue</h2>
|
||||||
<button class="btn btn-primary" onclick={loadQueue}>
|
<button class="btn btn-primary" onclick={loadQueue}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<Refresh class="h-5 w-5" />
|
||||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue