diff --git a/README.md b/README.md
index aec889ab..c8628000 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ Make sure to replace `/path/to/host` with your preferred root directory for conf
In the [config](./config/) directory are a couple of starter configuration files for prod and dev environments. The server expects a config.yaml in the config directory and will load settings from it when started.
-**Note:** You can set `email.host`, `email.port`, `email.email`, `email.password` and `jwt.secret` using environment variables `TW_EMAIL_HOST`, `TW_EMAIL_PORT`, `TW_EMAIL_SENDER`, `TW_EMAIL_PASSWORD` and `TW_JWT_SECRET` for improved security and flexibility. The server will fail to start if `jwt.secret` is left as `"secret"`, so be sure to set `TW_JWT_SECRET` or edit `config.yaml`.
+**Note:** You can set `email.host`, `email.port`, `email.email`, `email.password`, `jwt.secret`, and OAuth configuration using environment variables for improved security and flexibility. The server will fail to start if `jwt.secret` is left as `"secret"`, so be sure to set `TW_JWT_SECRET` or edit `config.yaml`.
The configuration files are yaml mappings with the following values:
@@ -94,6 +94,15 @@ The configuration files are yaml mappings with the following values:
| `jwt.secret` | `"secret"` | The secret key used for signing JWT tokens. **Make sure to change that or set `TW_JWT_SECRET`.** |
| `jwt.session_time` | `168h` | The duration for which a JWT session is valid. |
| `jwt.max_refresh` | `168h` | The maximum duration for refreshing a JWT session. |
+| `oauth.enabled` | `false` | Enable OAuth2 authentication. Can be set via `TW_OAUTH_ENABLED`. |
+| `oauth.client_id` | (empty) | OAuth2 client ID. Can be set via `TW_OAUTH_CLIENT_ID`. |
+| `oauth.client_secret` | (empty) | OAuth2 client secret. Can be set via `TW_OAUTH_CLIENT_SECRET`. |
+| `oauth.tenant_id` | (empty) | OAuth2 tenant ID (for Azure Entra). Can be set via `TW_OAUTH_TENANT_ID`. |
+| `oauth.authorize_url` | (empty) | OAuth2 authorization endpoint URL. Can be set via `TW_OAUTH_AUTHORIZE_URL`.|
+| `oauth.token_url` | (empty) | OAuth2 token endpoint URL. Can be set via `TW_OAUTH_TOKEN_URL`. |
+| `oauth.redirect_url` | (empty) | OAuth2 redirect URI. Can be set via `TW_OAUTH_REDIRECT_URL`. |
+| `oauth.scope` | (empty) | OAuth2 scope (e.g., `Tasks.ReadWrite`). Can be set via `TW_OAUTH_SCOPE`. |
+| `oauth.jwks_url` | (empty) | OAuth2 JWKS URL for token validation. Can be set via `TW_OAUTH_JWKS_URL`. |
| `server.host_name` | `localhost` | The hostname to use for external links. |
| `server.port` | `2021` | The port on which the server listens. |
| `server.read_timeout` | `2s` | The maximum duration for reading the entire request. |
@@ -116,6 +125,72 @@ The configuration files are yaml mappings with the following values:
| `email.email` | (empty) | The email address used for sending emails. |
| `email.password` | (empty) | The password for authenticating with the email server. |
+### 🔐 OAuth2 Configuration (Azure Entra ID Example)
+
+Task Wizard supports OAuth2 authentication as an alternative to username/password authentication. This is particularly useful when integrating with identity providers like Azure Entra ID (formerly Azure AD).
+
+#### Backend Configuration
+
+1. Register two applications in your identity provider:
+ - **Backend API App**: This will validate tokens and define scopes (e.g., `Tasks.ReadWrite`)
+ - **Frontend Client App**: This will initiate the OAuth flow
+
+2. Configure the backend via `config.yaml` or environment variables:
+
+```yaml
+oauth:
+ enabled: true
+ client_id: "your-backend-api-client-id"
+ client_secret: "your-backend-api-client-secret"
+ tenant_id: "your-tenant-id" # For Azure Entra ID
+ authorize_url: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize"
+ token_url: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token"
+ redirect_url: "https://your-domain.com/oauth/callback"
+ scope: "api://your-backend-api-client-id/Tasks.ReadWrite"
+ jwks_url: "https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys"
+```
+
+Or via environment variables:
+```bash
+TW_OAUTH_ENABLED=true
+TW_OAUTH_CLIENT_ID=your-backend-api-client-id
+TW_OAUTH_CLIENT_SECRET=your-backend-api-client-secret
+TW_OAUTH_TENANT_ID=your-tenant-id
+TW_OAUTH_AUTHORIZE_URL=https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
+TW_OAUTH_TOKEN_URL=https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
+TW_OAUTH_REDIRECT_URL=https://your-domain.com/oauth/callback
+TW_OAUTH_SCOPE=api://your-backend-api-client-id/Tasks.ReadWrite
+TW_OAUTH_JWKS_URL=https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys
+```
+
+#### Frontend Configuration
+
+Configure the frontend by setting environment variables during the build:
+
+```bash
+VITE_OAUTH_ENABLED=true
+VITE_OAUTH_CLIENT_ID=your-frontend-client-id
+VITE_OAUTH_AUTHORITY=https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
+VITE_OAUTH_SCOPE=api://your-backend-api-client-id/Tasks.ReadWrite
+VITE_OAUTH_REDIRECT_URI=https://your-domain.com/oauth/callback
+```
+
+#### Enabling OAuth in the UI
+
+1. Navigate to Settings > Feature Flags
+2. Enable the "Use OAuth 2.0 authentication" feature flag
+3. The login page will now show an "Sign in with OAuth" button
+
+**Note:** OAuth authentication and traditional username/password authentication can coexist. The feature flag controls which method is displayed to users.
+
+#### Security Considerations
+
+- **HTTPS Required**: OAuth must be used over HTTPS in production to protect tokens in transit
+- **Token Storage**: JWT tokens are stored in browser localStorage for session management. This is standard practice for SPA authentication, but means tokens are accessible to JavaScript code. Ensure your application is protected from XSS attacks.
+- **State Parameter**: OAuth state parameter is validated to prevent CSRF attacks
+- **Token Expiration**: Configure appropriate token expiration times in your OAuth provider
+- **Scope Restrictions**: Use minimal required scopes (e.g., `Tasks.ReadWrite`) to follow principle of least privilege
+
## 🛠️ Development
A [devcontainer](./.devcontainer/devcontainer.json) configuration is set up in this repo to help jumpstart development with all the required dependencies available for both the frontend and backend. You can use this configuration alongside
diff --git a/apiserver/config/config.go b/apiserver/config/config.go
index 907df7d0..4786de76 100644
--- a/apiserver/config/config.go
+++ b/apiserver/config/config.go
@@ -10,6 +10,7 @@ import (
type Config struct {
Database DatabaseConfig `mapstructure:"database" yaml:"database"`
Jwt JwtConfig `mapstructure:"jwt" yaml:"jwt"`
+ OAuth OAuthConfig `mapstructure:"oauth" yaml:"oauth"`
Server ServerConfig `mapstructure:"server" yaml:"server"`
SchedulerJobs SchedulerConfig `mapstructure:"scheduler_jobs" yaml:"scheduler_jobs"`
EmailConfig EmailConfig `mapstructure:"email" yaml:"email"`
@@ -26,6 +27,18 @@ type JwtConfig struct {
MaxRefresh time.Duration `mapstructure:"max_refresh" yaml:"max_refresh"`
}
+type OAuthConfig struct {
+ Enabled bool `mapstructure:"enabled" yaml:"enabled"`
+ ClientID string `mapstructure:"client_id" yaml:"client_id"`
+ ClientSecret string `mapstructure:"client_secret" yaml:"client_secret"`
+ TenantID string `mapstructure:"tenant_id" yaml:"tenant_id"`
+ AuthorizeURL string `mapstructure:"authorize_url" yaml:"authorize_url"`
+ TokenURL string `mapstructure:"token_url" yaml:"token_url"`
+ RedirectURL string `mapstructure:"redirect_url" yaml:"redirect_url"`
+ Scope string `mapstructure:"scope" yaml:"scope"`
+ JwksURL string `mapstructure:"jwks_url" yaml:"jwks_url"`
+}
+
type ServerConfig struct {
HostName string `mapstructure:"host_name" yaml:"host_name"`
Port int `mapstructure:"port" yaml:"port"`
@@ -75,6 +88,15 @@ func LoadConfig(configFile string) *Config {
// Allow values with secrets to be set via environment variables
_ = viper.BindEnv("jwt.secret", "TW_JWT_SECRET")
+ _ = viper.BindEnv("oauth.enabled", "TW_OAUTH_ENABLED")
+ _ = viper.BindEnv("oauth.client_id", "TW_OAUTH_CLIENT_ID")
+ _ = viper.BindEnv("oauth.client_secret", "TW_OAUTH_CLIENT_SECRET")
+ _ = viper.BindEnv("oauth.tenant_id", "TW_OAUTH_TENANT_ID")
+ _ = viper.BindEnv("oauth.authorize_url", "TW_OAUTH_AUTHORIZE_URL")
+ _ = viper.BindEnv("oauth.token_url", "TW_OAUTH_TOKEN_URL")
+ _ = viper.BindEnv("oauth.redirect_url", "TW_OAUTH_REDIRECT_URL")
+ _ = viper.BindEnv("oauth.scope", "TW_OAUTH_SCOPE")
+ _ = viper.BindEnv("oauth.jwks_url", "TW_OAUTH_JWKS_URL")
_ = viper.BindEnv("email.host", "TW_EMAIL_HOST")
_ = viper.BindEnv("email.port", "TW_EMAIL_PORT")
_ = viper.BindEnv("email.email", "TW_EMAIL_SENDER")
diff --git a/apiserver/config/config.oauth-example.yaml b/apiserver/config/config.oauth-example.yaml
new file mode 100644
index 00000000..b2fe6356
--- /dev/null
+++ b/apiserver/config/config.oauth-example.yaml
@@ -0,0 +1,58 @@
+# Example OAuth configuration for Azure Entra ID (formerly Azure AD)
+# Copy this file to config.yaml and update the values
+
+database:
+ migration: true
+ path: /config/task-wizard.db
+
+jwt:
+ secret: "CHANGEME-REPLACE-WITH-SECURE-SECRET" # IMPORTANT: Change this! Or set TW_JWT_SECRET env var
+ session_time: 168h # 7 days
+ max_refresh: 168h # 7 days
+
+oauth:
+ enabled: true
+ # Backend API application client ID (the one with exposed scopes)
+ client_id: "your-backend-api-client-id"
+ # Backend API application client secret
+ client_secret: "your-backend-api-client-secret"
+ # Azure tenant ID
+ tenant_id: "your-tenant-id"
+ # Azure OAuth2 authorization endpoint
+ authorize_url: "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/authorize"
+ # Azure OAuth2 token endpoint
+ token_url: "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/token"
+ # Redirect URL - must match what's configured in Azure app registration
+ redirect_url: "https://your-domain.com/oauth/callback"
+ # Scope - format is api://backend-client-id/scope-name
+ scope: "api://your-backend-api-client-id/Tasks.ReadWrite"
+ # JWKS URL for token validation
+ jwks_url: "https://login.microsoftonline.com/your-tenant-id/discovery/v2.0/keys"
+
+server:
+ host_name: your-domain.com
+ port: 2021
+ read_timeout: 2s
+ write_timeout: 1s
+ rate_period: 60s
+ rate_limit: 300
+ serve_frontend: true
+ registration: false # Typically false when using OAuth - users are managed through the OAuth provider
+ log_level: "info"
+ allowed_origins:
+ - "https://your-domain.com"
+ allow_cors_credentials: true
+
+scheduler_jobs:
+ due_frequency: 5m
+ overdue_frequency: 24h
+ password_reset_validity: 24h
+ token_expiration_reminder: 72h
+ notification_cleanup: 10m
+ token_expiration_cleanup: 24h
+
+email:
+ host: ""
+ port: 0
+ email: ""
+ password: ""
diff --git a/apiserver/config/config.yaml b/apiserver/config/config.yaml
index 62ec89bb..28821f30 100644
--- a/apiserver/config/config.yaml
+++ b/apiserver/config/config.yaml
@@ -5,6 +5,16 @@ jwt:
secret: "secret"
session_time: 168h
max_refresh: 168h
+oauth:
+ enabled: false
+ client_id: ""
+ client_secret: ""
+ tenant_id: ""
+ authorize_url: ""
+ token_url: ""
+ redirect_url: ""
+ scope: ""
+ jwks_url: ""
server:
host_name: example.com
port: 2021
diff --git a/apiserver/go.mod b/apiserver/go.mod
index 3e255647..7bd6f117 100644
--- a/apiserver/go.mod
+++ b/apiserver/go.mod
@@ -17,13 +17,16 @@ require (
)
require (
+ github.com/coreos/go-oidc/v3 v3.16.0
github.com/stretchr/testify v1.10.0
github.com/wneessen/go-mail v0.7.1
+ golang.org/x/oauth2 v0.32.0
gorm.io/driver/sqlite v1.5.7
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
diff --git a/apiserver/go.sum b/apiserver/go.sum
index 5196d911..be22d02e 100644
--- a/apiserver/go.sum
+++ b/apiserver/go.sum
@@ -10,6 +10,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
+github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -32,6 +34,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
+github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -163,6 +167,8 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
+golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/apiserver/internal/apis/oauth.go b/apiserver/internal/apis/oauth.go
new file mode 100644
index 00000000..d905593d
--- /dev/null
+++ b/apiserver/internal/apis/oauth.go
@@ -0,0 +1,225 @@
+package apis
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "dkhalife.com/tasks/core/config"
+ "dkhalife.com/tasks/core/internal/models"
+ uRepo "dkhalife.com/tasks/core/internal/repos/user"
+ "dkhalife.com/tasks/core/internal/services/logging"
+ auth "dkhalife.com/tasks/core/internal/utils/auth"
+ middleware "dkhalife.com/tasks/core/internal/utils/middleware"
+ "dkhalife.com/tasks/core/internal/utils/oauth"
+ jwt "github.com/appleboy/gin-jwt/v2"
+ "github.com/gin-gonic/gin"
+ limiter "github.com/ulule/limiter/v3"
+)
+
+type OAuthAPIHandler struct {
+ userRepo uRepo.IUserRepo
+ oauthProvider *oauth.OAuthProvider
+ cfg *config.Config
+ jwtMiddleware *jwt.GinJWTMiddleware
+}
+
+func OAuthAPI(
+ ur uRepo.IUserRepo,
+ cfg *config.Config,
+ jwtMiddleware *jwt.GinJWTMiddleware,
+) (*OAuthAPIHandler, error) {
+ provider, err := oauth.NewOAuthProvider(&cfg.OAuth)
+ if err != nil {
+ return nil, err
+ }
+
+ return &OAuthAPIHandler{
+ userRepo: ur,
+ oauthProvider: provider,
+ cfg: cfg,
+ jwtMiddleware: jwtMiddleware,
+ }, nil
+}
+
+// getOAuthConfig returns OAuth configuration for the frontend
+func (h *OAuthAPIHandler) getOAuthConfig(c *gin.Context) {
+ if !h.cfg.OAuth.Enabled {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "OAuth not configured",
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "enabled": h.cfg.OAuth.Enabled,
+ "client_id": h.cfg.OAuth.ClientID,
+ "authorize_url": h.cfg.OAuth.AuthorizeURL,
+ "scope": h.cfg.OAuth.Scope,
+ "redirect_url": h.cfg.OAuth.RedirectURL,
+ })
+}
+
+// initiateOAuth starts the OAuth flow by generating a state and returning the authorization URL
+func (h *OAuthAPIHandler) initiateOAuth(c *gin.Context) {
+ if h.oauthProvider == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "OAuth not configured",
+ })
+ return
+ }
+
+ state, err := oauth.GenerateStateToken()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "Failed to generate state token",
+ })
+ return
+ }
+
+ // Store state in session or cookie for validation
+ // For now, we'll return it and expect the client to send it back
+ authURL := h.oauthProvider.GetAuthorizationURL(state)
+
+ c.JSON(http.StatusOK, gin.H{
+ "authorization_url": authURL,
+ "state": state,
+ })
+}
+
+// callbackOAuth handles the OAuth callback
+func (h *OAuthAPIHandler) callbackOAuth(c *gin.Context) {
+ if h.oauthProvider == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "OAuth not configured",
+ })
+ return
+ }
+
+ log := logging.FromContext(c)
+
+ code := c.Query("code")
+ if code == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "Authorization code not provided",
+ })
+ return
+ }
+
+ // Exchange code for token
+ token, err := h.oauthProvider.ExchangeCode(c.Request.Context(), code)
+ if err != nil {
+ log.Errorf("Failed to exchange code: %s", err.Error())
+ c.JSON(http.StatusUnauthorized, gin.H{
+ "error": "Failed to exchange authorization code",
+ })
+ return
+ }
+
+ // Extract ID token if available
+ idToken, ok := token.Extra("id_token").(string)
+ if !ok || idToken == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{
+ "error": "ID token not found in response",
+ })
+ return
+ }
+
+ // Validate the ID token
+ claims, err := h.oauthProvider.ValidateToken(c.Request.Context(), idToken)
+ if err != nil {
+ log.Errorf("Failed to validate token: %s", err.Error())
+ c.JSON(http.StatusUnauthorized, gin.H{
+ "error": "Failed to validate token",
+ })
+ return
+ }
+
+ // Check if user exists, if not create one
+ email := claims.Email
+ if email == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "Email not found in token claims",
+ })
+ return
+ }
+
+ user, err := h.userRepo.FindByEmail(c.Request.Context(), email)
+ if err != nil {
+ // User doesn't exist, create one
+ displayName := claims.Name
+ if displayName == "" {
+ displayName = claims.PreferredUsername
+ }
+ if displayName == "" {
+ displayName = email
+ }
+
+ user = &models.User{
+ Email: email,
+ DisplayName: displayName,
+ Password: "", // No password for OAuth users
+ Disabled: false,
+ }
+
+ if err := h.userRepo.CreateUser(c.Request.Context(), user); err != nil {
+ log.Errorf("Failed to create user: %s", err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "Failed to create user",
+ })
+ return
+ }
+
+ // Refetch user to get ID
+ user, err = h.userRepo.FindByEmail(c.Request.Context(), email)
+ if err != nil {
+ log.Errorf("Failed to fetch newly created user: %s", err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "Failed to fetch user",
+ })
+ return
+ }
+ }
+
+ // Check if user is disabled
+ if user.Disabled {
+ c.JSON(http.StatusUnauthorized, gin.H{
+ "error": "User account is disabled",
+ })
+ return
+ }
+
+ // Generate JWT token for the user
+ tokenString, expire, err := h.jwtMiddleware.TokenGenerator(map[string]interface{}{
+ auth.IdentityKey: fmt.Sprintf("%d", user.ID),
+ "type": "user",
+ "scopes": []string{"task:read", "task:write", "label:read", "label:write", "user:read", "user:write", "token:write"},
+ })
+ if err != nil {
+ log.Errorf("Failed to generate JWT: %s", err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "Failed to generate session token",
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "token": tokenString,
+ "expiration": expire.Format(time.RFC3339),
+ })
+}
+
+func OAuthRoutes(router *gin.Engine, h *OAuthAPIHandler, limiter *limiter.Limiter) {
+ if h == nil || h.oauthProvider == nil {
+ // OAuth not configured, skip routes
+ return
+ }
+
+ oauthRoutes := router.Group("api/v1/oauth")
+ oauthRoutes.Use(middleware.RateLimitMiddleware(limiter))
+ {
+ oauthRoutes.GET("/config", h.getOAuthConfig)
+ oauthRoutes.GET("/authorize", h.initiateOAuth)
+ oauthRoutes.GET("/callback", h.callbackOAuth)
+ }
+}
diff --git a/apiserver/internal/utils/oauth/oauth.go b/apiserver/internal/utils/oauth/oauth.go
new file mode 100644
index 00000000..7e141101
--- /dev/null
+++ b/apiserver/internal/utils/oauth/oauth.go
@@ -0,0 +1,138 @@
+package oauth
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "time"
+
+ "dkhalife.com/tasks/core/config"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+// OAuthProvider wraps OAuth2 and OIDC functionality
+type OAuthProvider struct {
+ config *oauth2.Config
+ verifier *oidc.IDTokenVerifier
+ cfg *config.OAuthConfig
+}
+
+// NewOAuthProvider creates a new OAuth provider
+func NewOAuthProvider(cfg *config.OAuthConfig) (*OAuthProvider, error) {
+ if !cfg.Enabled {
+ return nil, nil
+ }
+
+ // Validate required fields
+ if cfg.ClientID == "" {
+ return nil, errors.New("oauth client_id is required")
+ }
+ if cfg.ClientSecret == "" {
+ return nil, errors.New("oauth client_secret is required")
+ }
+ if cfg.AuthorizeURL == "" {
+ return nil, errors.New("oauth authorize_url is required")
+ }
+ if cfg.TokenURL == "" {
+ return nil, errors.New("oauth token_url is required")
+ }
+ if cfg.RedirectURL == "" {
+ return nil, errors.New("oauth redirect_url is required")
+ }
+ if cfg.Scope == "" {
+ return nil, errors.New("oauth scope is required")
+ }
+
+ oauth2Config := &oauth2.Config{
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ RedirectURL: cfg.RedirectURL,
+ Endpoint: oauth2.Endpoint{
+ AuthURL: cfg.AuthorizeURL,
+ TokenURL: cfg.TokenURL,
+ },
+ Scopes: []string{cfg.Scope},
+ }
+
+ // Setup OIDC verifier if JWKS URL is provided
+ var verifier *oidc.IDTokenVerifier
+ if cfg.JwksURL != "" {
+ ctx := context.Background()
+ keySet := oidc.NewRemoteKeySet(ctx, cfg.JwksURL)
+ verifier = oidc.NewVerifier(cfg.ClientID, keySet, &oidc.Config{
+ ClientID: cfg.ClientID,
+ SkipExpiryCheck: false,
+ SkipIssuerCheck: true, // We may need to be flexible with issuer
+ SkipClientIDCheck: false,
+ })
+ }
+
+ return &OAuthProvider{
+ config: oauth2Config,
+ verifier: verifier,
+ cfg: cfg,
+ }, nil
+}
+
+// GenerateStateToken generates a random state token for OAuth2 flow
+func GenerateStateToken() (string, error) {
+ b := make([]byte, 32)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "", err
+ }
+ return base64.URLEncoding.EncodeToString(b), nil
+}
+
+// GetAuthorizationURL returns the OAuth2 authorization URL
+func (p *OAuthProvider) GetAuthorizationURL(state string) string {
+ return p.config.AuthCodeURL(state, oauth2.AccessTypeOffline)
+}
+
+// ExchangeCode exchanges an authorization code for tokens
+func (p *OAuthProvider) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) {
+ return p.config.Exchange(ctx, code)
+}
+
+// ValidateToken validates an OAuth2 access token
+func (p *OAuthProvider) ValidateToken(ctx context.Context, token string) (*Claims, error) {
+ if p.verifier == nil {
+ return nil, errors.New("token validation not configured")
+ }
+
+ // Verify the ID token
+ idToken, err := p.verifier.Verify(ctx, token)
+ if err != nil {
+ return nil, fmt.Errorf("failed to verify token: %w", err)
+ }
+
+ var claims Claims
+ if err := idToken.Claims(&claims); err != nil {
+ return nil, fmt.Errorf("failed to parse claims: %w", err)
+ }
+
+ return &claims, nil
+}
+
+// Claims represents the claims in an OAuth2 token
+type Claims struct {
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ Name string `json:"name"`
+ PreferredUsername string `json:"preferred_username"`
+ Sub string `json:"sub"`
+ Roles []string `json:"roles"`
+ Scope string `json:"scp"`
+ Aud string `json:"aud"`
+ Exp int64 `json:"exp"`
+ Iat int64 `json:"iat"`
+ Iss string `json:"iss"`
+}
+
+// IsExpired checks if the token is expired
+func (c *Claims) IsExpired() bool {
+ return time.Now().Unix() > c.Exp
+}
diff --git a/apiserver/internal/utils/oauth/oauth_test.go b/apiserver/internal/utils/oauth/oauth_test.go
new file mode 100644
index 00000000..e3b21d84
--- /dev/null
+++ b/apiserver/internal/utils/oauth/oauth_test.go
@@ -0,0 +1,173 @@
+package oauth
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "dkhalife.com/tasks/core/config"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+)
+
+type OAuthTestSuite struct {
+ suite.Suite
+}
+
+func TestOAuthTestSuite(t *testing.T) {
+ suite.Run(t, new(OAuthTestSuite))
+}
+
+func (suite *OAuthTestSuite) TestNewOAuthProvider_Disabled() {
+ cfg := &config.OAuthConfig{
+ Enabled: false,
+ }
+
+ provider, err := NewOAuthProvider(cfg)
+
+ assert.Nil(suite.T(), err)
+ assert.Nil(suite.T(), provider)
+}
+
+func (suite *OAuthTestSuite) TestNewOAuthProvider_MissingClientID() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientSecret: "secret",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ }
+
+ provider, err := NewOAuthProvider(cfg)
+
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), provider)
+ assert.Contains(suite.T(), err.Error(), "client_id")
+}
+
+func (suite *OAuthTestSuite) TestNewOAuthProvider_MissingClientSecret() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientID: "client-id",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ }
+
+ provider, err := NewOAuthProvider(cfg)
+
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), provider)
+ assert.Contains(suite.T(), err.Error(), "client_secret")
+}
+
+func (suite *OAuthTestSuite) TestNewOAuthProvider_Success() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ }
+
+ provider, err := NewOAuthProvider(cfg)
+
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), provider)
+ assert.Equal(suite.T(), cfg.ClientID, provider.config.ClientID)
+ assert.Equal(suite.T(), cfg.ClientSecret, provider.config.ClientSecret)
+}
+
+func (suite *OAuthTestSuite) TestGenerateStateToken() {
+ token1, err1 := GenerateStateToken()
+ token2, err2 := GenerateStateToken()
+
+ assert.Nil(suite.T(), err1)
+ assert.Nil(suite.T(), err2)
+ assert.NotEmpty(suite.T(), token1)
+ assert.NotEmpty(suite.T(), token2)
+ assert.NotEqual(suite.T(), token1, token2)
+}
+
+func (suite *OAuthTestSuite) TestGetAuthorizationURL() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ }
+
+ provider, _ := NewOAuthProvider(cfg)
+ state := "test-state"
+
+ url := provider.GetAuthorizationURL(state)
+
+ assert.Contains(suite.T(), url, "https://example.com/authorize")
+ assert.Contains(suite.T(), url, "client_id=client-id")
+ assert.Contains(suite.T(), url, "state=test-state")
+ assert.Contains(suite.T(), url, "redirect_uri=https%3A%2F%2Fexample.com%2Fcallback")
+}
+
+func (suite *OAuthTestSuite) TestExchangeCode_InvalidCode() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ }
+
+ provider, _ := NewOAuthProvider(cfg)
+ ctx := context.Background()
+
+ // This will fail because we're not hitting a real OAuth server
+ token, err := provider.ExchangeCode(ctx, "invalid-code")
+
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), token)
+}
+
+func (suite *OAuthTestSuite) TestValidateToken_NoVerifier() {
+ cfg := &config.OAuthConfig{
+ Enabled: true,
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ AuthorizeURL: "https://example.com/authorize",
+ TokenURL: "https://example.com/token",
+ RedirectURL: "https://example.com/callback",
+ Scope: "openid",
+ // No JwksURL, so no verifier
+ }
+
+ provider, _ := NewOAuthProvider(cfg)
+ ctx := context.Background()
+
+ claims, err := provider.ValidateToken(ctx, "some-token")
+
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), claims)
+ assert.Contains(suite.T(), err.Error(), "not configured")
+}
+
+func (suite *OAuthTestSuite) TestClaimsIsExpired() {
+ // Not expired
+ claims := &Claims{
+ Exp: time.Now().Add(1 * time.Hour).Unix(),
+ }
+ assert.False(suite.T(), claims.IsExpired())
+
+ // Expired
+ expiredClaims := &Claims{
+ Exp: time.Now().Add(-1 * time.Hour).Unix(),
+ }
+ assert.True(suite.T(), expiredClaims.IsExpired())
+}
diff --git a/apiserver/main.go b/apiserver/main.go
index 3a796652..4388c532 100644
--- a/apiserver/main.go
+++ b/apiserver/main.go
@@ -100,6 +100,7 @@ func main() {
fx.Provide(apis.LabelsAPI),
fx.Provide(apis.LogsAPI),
fx.Provide(apis.CalDAVAPI),
+ fx.Provide(apis.OAuthAPI),
fx.Provide(frontend.NewHandler),
fx.Provide(backend.NewHandler),
@@ -110,6 +111,7 @@ func main() {
apis.LabelRoutes,
apis.CalDAVRoutes,
apis.LogRoutes,
+ apis.OAuthRoutes,
ws.Routes,
tService.TaskMessages,
lService.LabelMessages,
diff --git a/frontend/.env b/frontend/.env
index 0854608f..190d25b3 100644
--- a/frontend/.env
+++ b/frontend/.env
@@ -1,2 +1,7 @@
VITE_APP_API_URL=http://localhost:2021
VITE_BACKEND_VERSION=0.0.0 # To be set during packaging step
+VITE_OAUTH_ENABLED=false
+VITE_OAUTH_CLIENT_ID=
+VITE_OAUTH_AUTHORITY=
+VITE_OAUTH_SCOPE=
+VITE_OAUTH_REDIRECT_URI=
diff --git a/frontend/.env.oauth-example b/frontend/.env.oauth-example
new file mode 100644
index 00000000..f5eaec99
--- /dev/null
+++ b/frontend/.env.oauth-example
@@ -0,0 +1,29 @@
+# Example OAuth configuration for frontend
+# Copy this file to .env or .env.prod and update the values
+
+# API URL - the backend server
+VITE_APP_API_URL=https://your-domain.com
+
+# Backend version (automatically set during build - do not manually edit)
+VITE_BACKEND_VERSION=BUILD_TIME_REPLACEMENT
+
+# OAuth Configuration
+# Enable OAuth authentication
+VITE_OAUTH_ENABLED=true
+
+# Frontend application client ID (public OAuth client - NOT the backend API client ID)
+# This is for the client that initiates the OAuth flow.
+# Backend client ID is for the confidential API client that validates tokens.
+VITE_OAUTH_CLIENT_ID=your-frontend-client-id
+
+# OAuth authority URL (authorization endpoint)
+# For Azure Entra ID: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
+VITE_OAUTH_AUTHORITY=https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/authorize
+
+# OAuth scope - this should match the scope exposed by the backend API
+# Format for Azure: api://backend-client-id/scope-name
+VITE_OAUTH_SCOPE=api://your-backend-api-client-id/Tasks.ReadWrite
+
+# OAuth redirect URI - where users are sent after authentication
+# Must match the redirect URI configured in your app registration
+VITE_OAUTH_REDIRECT_URI=https://your-domain.com/oauth/callback
diff --git a/frontend/src/constants/featureFlags.ts b/frontend/src/constants/featureFlags.ts
index 4d824496..d8f47d7a 100644
--- a/frontend/src/constants/featureFlags.ts
+++ b/frontend/src/constants/featureFlags.ts
@@ -1,6 +1,6 @@
import { retrieveValue, storeValue } from '@/utils/storage'
-export type FeatureFlag = 'useWebsockets' | 'refreshStaleData'
+export type FeatureFlag = 'useWebsockets' | 'refreshStaleData' | 'useOAuth'
export interface FeatureFlagDefinition {
name: FeatureFlag
@@ -19,6 +19,11 @@ export const featureFlagDefinitions: FeatureFlagDefinition[] = [
description: 'Refresh stale data when tab becomes visible',
defaultValue: false,
},
+ {
+ name: 'useOAuth',
+ description: 'Use OAuth 2.0 authentication',
+ defaultValue: false,
+ },
]
export const FEATURE_FLAG_PREFIX = 'featureFlags.'
diff --git a/frontend/src/contexts/RouterContext.tsx b/frontend/src/contexts/RouterContext.tsx
index b4d420d3..ba334c84 100644
--- a/frontend/src/contexts/RouterContext.tsx
+++ b/frontend/src/contexts/RouterContext.tsx
@@ -7,6 +7,7 @@ import { ForgotPasswordView } from '@/views/Authorization/ForgotPasswordView'
import { LoginView } from '@/views/Authorization/LoginView'
import { SignupView } from '@/views/Authorization/Signup'
import { UpdatePasswordView } from '@/views/Authorization/UpdatePasswordView'
+import { OAuthCallbackView } from '@/views/OAuth/OAuthCallbackView'
import { TaskHistory } from '@/views/History/TaskHistory'
import { LabelView } from '@/views/Labels/LabelView'
import { NotFound } from '@/views/NotFound'
@@ -183,6 +184,10 @@ class RouterContextImpl extends React.Component}
/>
+ }
+ />
}
diff --git a/frontend/src/utils/oauth.ts b/frontend/src/utils/oauth.ts
new file mode 100644
index 00000000..b960d762
--- /dev/null
+++ b/frontend/src/utils/oauth.ts
@@ -0,0 +1,103 @@
+import { Request } from './api'
+
+export interface OAuthConfig {
+ enabled: boolean
+ client_id: string
+ authorize_url: string
+ scope: string
+ redirect_url: string
+}
+
+export interface OAuthInitiateResponse {
+ authorization_url: string
+ state: string
+}
+
+export interface OAuthTokenResponse {
+ token: string
+ expiration: string
+}
+
+// Get OAuth configuration from the backend
+export const getOAuthConfig = async (): Promise => {
+ try {
+ const config = await Request(
+ '/oauth/config',
+ 'GET',
+ undefined,
+ false,
+ )
+ return config
+ } catch (error) {
+ console.error('Failed to get OAuth config:', error)
+ return null
+ }
+}
+
+// Initiate OAuth flow - get authorization URL
+export const initiateOAuth = async (): Promise => {
+ return await Request(
+ '/oauth/authorize',
+ 'GET',
+ undefined,
+ false,
+ )
+}
+
+// Complete OAuth flow by exchanging code for token
+export const completeOAuth = async (
+ code: string,
+): Promise => {
+ return await Request(
+ `/oauth/callback?code=${encodeURIComponent(code)}`,
+ 'GET',
+ undefined,
+ false,
+ )
+}
+
+// Generate a random string for state parameter
+export const generateState = (): string => {
+ const array = new Uint8Array(32)
+ crypto.getRandomValues(array)
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(
+ '',
+ )
+}
+
+// Store OAuth state in session storage
+export const storeOAuthState = (state: string): void => {
+ sessionStorage.setItem('oauth_state', state)
+}
+
+// Retrieve and validate OAuth state from session storage
+export const validateOAuthState = (state: string): boolean => {
+ const storedState = sessionStorage.getItem('oauth_state')
+ sessionStorage.removeItem('oauth_state')
+ return storedState === state
+}
+
+// Check if OAuth is configured via environment variables
+export const isOAuthConfiguredViaEnv = (): boolean => {
+ const enabled = import.meta.env.VITE_OAUTH_ENABLED === 'true'
+ const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID
+ const authority = import.meta.env.VITE_OAUTH_AUTHORITY
+ const scope = import.meta.env.VITE_OAUTH_SCOPE
+
+ return enabled && !!clientId && !!authority && !!scope
+}
+
+// Get OAuth configuration from environment variables
+export const getOAuthConfigFromEnv = (): OAuthConfig | null => {
+ if (!isOAuthConfiguredViaEnv()) {
+ return null
+ }
+
+ return {
+ enabled: true,
+ client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
+ authorize_url: import.meta.env.VITE_OAUTH_AUTHORITY,
+ scope: import.meta.env.VITE_OAUTH_SCOPE,
+ redirect_url: import.meta.env.VITE_OAUTH_REDIRECT_URI || window.location.origin + '/oauth/callback',
+ }
+}
diff --git a/frontend/src/views/Authorization/LoginView.tsx b/frontend/src/views/Authorization/LoginView.tsx
index 5d4284ba..99eafbf3 100644
--- a/frontend/src/views/Authorization/LoginView.tsx
+++ b/frontend/src/views/Authorization/LoginView.tsx
@@ -13,17 +13,27 @@ import { doLogin } from '@/utils/auth'
import { setTitle } from '@/utils/dom'
import { NavigationPaths, WithNavigate } from '@/utils/navigation'
import { connect } from 'react-redux'
-import { AppDispatch } from '@/store/store'
+import { AppDispatch, RootState } from '@/store/store'
import { pushStatus } from '@/store/statusSlice'
import { StatusSeverity } from '@/models/status'
+import {
+ getOAuthConfig,
+ getOAuthConfigFromEnv,
+ initiateOAuth,
+ isOAuthConfiguredViaEnv,
+ storeOAuthState,
+} from '@/utils/oauth'
type LoginViewProps = WithNavigate & {
pushStatus: (message: string, severity: StatusSeverity, timeout?: number) => void
+ useOAuth: boolean
}
interface LoginViewState {
email: string
password: string
+ oauthAvailable: boolean
+ oauthLoading: boolean
}
class LoginViewImpl extends React.Component {
@@ -33,11 +43,31 @@ class LoginViewImpl extends React.Component {
this.state = {
email: '',
password: '',
+ oauthAvailable: false,
+ oauthLoading: false,
}
}
- componentDidMount(): void {
+ async componentDidMount(): Promise {
setTitle('Login')
+
+ // Check if OAuth is enabled via feature flag
+ if (this.props.useOAuth) {
+ // First check if OAuth is configured via environment variables
+ if (isOAuthConfiguredViaEnv()) {
+ this.setState({ oauthAvailable: true })
+ } else {
+ // Otherwise, check with the backend
+ try {
+ const config = await getOAuthConfig()
+ if (config && config.enabled) {
+ this.setState({ oauthAvailable: true })
+ }
+ } catch (error) {
+ console.error('Failed to check OAuth configuration:', error)
+ }
+ }
+ }
}
private handleSubmit = async (e: React.MouseEvent | React.FormEvent) => {
@@ -59,8 +89,33 @@ class LoginViewImpl extends React.Component {
this.setState({ password: e.target.value })
}
+ private handleOAuthLogin = async () => {
+ this.setState({ oauthLoading: true })
+ try {
+ // Check if OAuth is configured via environment variables
+ const envConfig = getOAuthConfigFromEnv()
+ if (envConfig) {
+ // Use environment variable configuration
+ const state = crypto.getRandomValues(new Uint8Array(32)).reduce((acc, val) => acc + val.toString(16).padStart(2, '0'), '')
+ storeOAuthState(state)
+
+ const authUrl = `${envConfig.authorize_url}?client_id=${encodeURIComponent(envConfig.client_id)}&redirect_uri=${encodeURIComponent(envConfig.redirect_url)}&response_type=code&scope=${encodeURIComponent(envConfig.scope)}&state=${state}`
+ window.location.href = authUrl
+ } else {
+ // Use backend-provided configuration
+ const response = await initiateOAuth()
+ storeOAuthState(response.state)
+ window.location.href = response.authorization_url
+ }
+ } catch (error) {
+ this.setState({ oauthLoading: false })
+ this.props.pushStatus((error as Error).message, 'error', 5000)
+ }
+ }
+
render(): React.ReactNode {
- const { navigate } = this.props
+ const { navigate, useOAuth } = this.props
+ const { oauthAvailable, oauthLoading } = this.state
return (
{
Sign in to your account to continue
+
+ {useOAuth && oauthAvailable ? (
+ <>
+
+ or
+ >
+ ) : null}
+
Email
@@ -162,9 +240,16 @@ class LoginViewImpl extends React.Component {
}
}
+const mapStateToProps = (state: RootState) => ({
+ useOAuth: state.featureFlags.useOAuth,
+})
+
const mapDispatchToProps = (dispatch: AppDispatch) => ({
pushStatus: (message: string, severity: StatusSeverity, timeout?: number) =>
dispatch(pushStatus({ message, severity, timeout })),
})
-export const LoginView = connect(null, mapDispatchToProps)(LoginViewImpl)
+export const LoginView = connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(LoginViewImpl)
diff --git a/frontend/src/views/OAuth/OAuthCallbackView.tsx b/frontend/src/views/OAuth/OAuthCallbackView.tsx
new file mode 100644
index 00000000..e973fcd5
--- /dev/null
+++ b/frontend/src/views/OAuth/OAuthCallbackView.tsx
@@ -0,0 +1,107 @@
+import { Box, CircularProgress, Container, Typography } from '@mui/joy'
+import React from 'react'
+import { NavigationPaths, WithNavigate } from '@/utils/navigation'
+import { completeOAuth, validateOAuthState } from '@/utils/oauth'
+import { setTitle } from '@/utils/dom'
+
+type OAuthCallbackViewProps = WithNavigate
+
+class OAuthCallbackViewImpl extends React.Component {
+ componentDidMount(): void {
+ setTitle('OAuth Callback')
+ this.handleCallback()
+ }
+
+ private handleCallback = async () => {
+ try {
+ // Extract code and state from URL
+ const urlParams = new URLSearchParams(window.location.search)
+ const code = urlParams.get('code')
+ const state = urlParams.get('state')
+ const error = urlParams.get('error')
+ const errorDescription = urlParams.get('error_description')
+
+ // Check for errors from OAuth provider
+ if (error) {
+ console.error('OAuth error:', error, errorDescription)
+ this.props.navigate(
+ NavigationPaths.Login +
+ '?error=' +
+ encodeURIComponent(errorDescription || error),
+ )
+ return
+ }
+
+ // Validate required parameters
+ if (!code || !state) {
+ console.error('Missing code or state in OAuth callback')
+ this.props.navigate(
+ NavigationPaths.Login +
+ '?error=' +
+ encodeURIComponent('Invalid OAuth callback'),
+ )
+ return
+ }
+
+ // Validate state to prevent CSRF
+ if (!validateOAuthState(state)) {
+ console.error('Invalid OAuth state')
+ this.props.navigate(
+ NavigationPaths.Login +
+ '?error=' +
+ encodeURIComponent('Invalid OAuth state'),
+ )
+ return
+ }
+
+ // Exchange code for token
+ const response = await completeOAuth(code)
+ localStorage.setItem('ca_token', response.token)
+ localStorage.setItem('ca_expiration', response.expiration)
+
+ // Navigate to home or redirect URL
+ const redirectUrl = localStorage.getItem('ca_redirect')
+ if (redirectUrl) {
+ localStorage.removeItem('ca_redirect')
+ this.props.navigate(redirectUrl)
+ } else {
+ this.props.navigate(NavigationPaths.HomeView())
+ }
+ } catch (error) {
+ console.error('Failed to complete OAuth flow:', error)
+ this.props.navigate(
+ NavigationPaths.Login +
+ '?error=' +
+ encodeURIComponent('Failed to complete authentication'),
+ )
+ }
+ }
+
+ render(): React.ReactNode {
+ return (
+
+
+
+
+ Completing sign in...
+
+
+
+ )
+ }
+}
+
+export const OAuthCallbackView = OAuthCallbackViewImpl
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 160a3078..90294243 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -32,7 +32,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz"
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
-"@babel/core@^7.0.0", "@babel/core@^7.0.0 || ^8.0.0-0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.0 || ^8.0.0-0", "@babel/core@^7.11.0 || ^8.0.0-beta.1", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@>=7.0.0-beta.0 <8":
+"@babel/core@^7.23.9", "@babel/core@^7.27.4":
version "7.28.4"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz"
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
@@ -309,16 +309,38 @@
"@csstools/color-helpers" "^5.1.0"
"@csstools/css-calc" "^2.1.4"
-"@csstools/css-parser-algorithms@^3.0.4", "@csstools/css-parser-algorithms@^3.0.5":
+"@csstools/css-parser-algorithms@^3.0.4":
version "3.0.5"
resolved "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz"
integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==
-"@csstools/css-tokenizer@^3.0.3", "@csstools/css-tokenizer@^3.0.4":
+"@csstools/css-tokenizer@^3.0.3":
version "3.0.4"
resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz"
integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==
+"@emnapi/core@^1.4.3":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0"
+ integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==
+ dependencies:
+ "@emnapi/wasi-threads" "1.1.0"
+ tslib "^2.4.0"
+
+"@emnapi/runtime@^1.4.3":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73"
+ integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==
+ dependencies:
+ tslib "^2.4.0"
+
+"@emnapi/wasi-threads@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
+ integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
+ dependencies:
+ tslib "^2.4.0"
+
"@emotion/babel-plugin@^11.13.5":
version "11.13.5"
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
@@ -364,7 +386,7 @@
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
-"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0":
+"@emotion/react@^11.14.0":
version "11.14.0"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
@@ -394,7 +416,7 @@
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
-"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0":
+"@emotion/styled@^11.14.0":
version "11.14.1"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
@@ -426,11 +448,136 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
+"@esbuild/aix-ppc64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.7.tgz#6b9c2f9bc106e1cbb18d906a7b2f3ec77dcb114a"
+ integrity sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==
+
+"@esbuild/android-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.7.tgz#eb5621506f95ed36c4dc5b2ccda107e4318c8ca2"
+ integrity sha512-p0ohDnwyIbAtztHTNUTzN5EGD/HJLs1bwysrOPgSdlIA6NDnReoVfoCyxG6W1d85jr2X80Uq5KHftyYgaK9LPQ==
+
+"@esbuild/android-arm@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.7.tgz#cd6140b05fb2a73a0535e96b5f2e91ac24ce6d17"
+ integrity sha512-Jhuet0g1k9rAJHrXGIh7sFknFuT4sfytYZpZpuZl7YKDhnPByVAm5oy2LEBmMbuYf3ejWVYCc2seX81Mk+madA==
+
+"@esbuild/android-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.7.tgz#7a55c3613875d11ab42eb9365bd0b147d992e4c0"
+ integrity sha512-mMxIJFlSgVK23HSsII3ZX9T2xKrBCDGyk0qiZnIW10LLFFtZLkFD6imZHu7gUo2wkNZwS9Yj3mOtZD3ZPcjCcw==
+
+"@esbuild/darwin-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.7.tgz#c10003a7ff8d3f6da05e879bc341854f282bafc5"
+ integrity sha512-jyOFLGP2WwRwxM8F1VpP6gcdIJc8jq2CUrURbbTouJoRO7XCkU8GdnTDFIHdcifVBT45cJlOYsZ1kSlfbKjYUQ==
+
+"@esbuild/darwin-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.7.tgz#4fe71eb6315f21fbc93381a5ebb9c5b462559e48"
+ integrity sha512-m9bVWqZCwQ1BthruifvG64hG03zzz9gE2r/vYAhztBna1/+qXiHyP9WgnyZqHgGeXoimJPhAmxfbeU+nMng6ZA==
+
+"@esbuild/freebsd-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.7.tgz#af4af1d74867cbd838682543b71e23f205fcc9fa"
+ integrity sha512-Bss7P4r6uhr3kDzRjPNEnTm/oIBdTPRNQuwaEFWT/uvt6A1YzK/yn5kcx5ZxZ9swOga7LqeYlu7bDIpDoS01bA==
+
+"@esbuild/freebsd-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.7.tgz#628d9525a9b56c17d6c43e73272c894ee1acbf7b"
+ integrity sha512-S3BFyjW81LXG7Vqmr37ddbThrm3A84yE7ey/ERBlK9dIiaWgrjRlre3pbG7txh1Uaxz8N7wGGQXmC9zV+LIpBQ==
+
+"@esbuild/linux-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.7.tgz#dfc3633a62b331a8bd3cc8ba538a11123fa79cba"
+ integrity sha512-HfQZQqrNOfS1Okn7PcsGUqHymL1cWGBslf78dGvtrj8q7cN3FkapFgNA4l/a5lXDwr7BqP2BSO6mz9UremNPbg==
+
+"@esbuild/linux-arm@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.7.tgz#a22d7f233809164f8fca9dde003543f6becd05d6"
+ integrity sha512-JZMIci/1m5vfQuhKoFXogCKVYVfYQmoZJg8vSIMR4TUXbF+0aNlfXH3DGFEFMElT8hOTUF5hisdZhnrZO/bkDw==
+
+"@esbuild/linux-ia32@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.7.tgz#ea97ec2b1e830ca64ec282acf9f6167af2828f58"
+ integrity sha512-9Jex4uVpdeofiDxnwHRgen+j6398JlX4/6SCbbEFEXN7oMO2p0ueLN+e+9DdsdPLUdqns607HmzEFnxwr7+5wQ==
+
+"@esbuild/linux-loong64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.7.tgz#9b6db65f168eb5e71b62a345769db6add5b6208b"
+ integrity sha512-TG1KJqjBlN9IHQjKVUYDB0/mUGgokfhhatlay8aZ/MSORMubEvj/J1CL8YGY4EBcln4z7rKFbsH+HeAv0d471w==
+
+"@esbuild/linux-mips64el@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.7.tgz#9809b5ea6659dd5032d196e5605246a376e3172e"
+ integrity sha512-Ty9Hj/lx7ikTnhOfaP7ipEm/ICcBv94i/6/WDg0OZ3BPBHhChsUbQancoWYSO0WNkEiSW5Do4febTTy4x1qYQQ==
+
+"@esbuild/linux-ppc64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.7.tgz#fd0de775f7e07af9655e9471d9b29cafebcb3420"
+ integrity sha512-MrOjirGQWGReJl3BNQ58BLhUBPpWABnKrnq8Q/vZWWwAB1wuLXOIxS2JQ1LT3+5T+3jfPh0tyf5CpbyQHqnWIQ==
+
+"@esbuild/linux-riscv64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.7.tgz#58f98119dea0b85bedb80aa982a1d4779fd5d7a5"
+ integrity sha512-9pr23/pqzyqIZEZmQXnFyqp3vpa+KBk5TotfkzGMqpw089PGm0AIowkUppHB9derQzqniGn3wVXgck19+oqiOw==
+
+"@esbuild/linux-s390x@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.7.tgz#24ca2e4b4dbc99b76458a7e6f9235368192c9c24"
+ integrity sha512-4dP11UVGh9O6Y47m8YvW8eoA3r8qL2toVZUbBKyGta8j6zdw1cn9F/Rt59/Mhv0OgY68pHIMjGXWOUaykCnx+w==
+
"@esbuild/linux-x64@0.25.7":
version "0.25.7"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.7.tgz"
integrity sha512-ghJMAJTdw/0uhz7e7YnpdX1xVn7VqA0GrWrAO2qKMuqbvgHT2VZiBv1BQ//VcHsPir4wsL3P2oPggfKPzTKoCA==
+"@esbuild/netbsd-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.7.tgz#0787eaea8d77206dc399b071c850a52c171a60a6"
+ integrity sha512-bwXGEU4ua45+u5Ci/a55B85KWaDSRS8NPOHtxy2e3etDjbz23wlry37Ffzapz69JAGGc4089TBo+dGzydQmydg==
+
+"@esbuild/netbsd-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.7.tgz#72e32d765597ebadb797f70014ef7222470208f5"
+ integrity sha512-tUZRvLtgLE5OyN46sPSYlgmHoBS5bx2URSrgZdW1L1teWPYVmXh+QN/sKDqkzBo/IHGcKcHLKDhBeVVkO7teEA==
+
+"@esbuild/openbsd-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.7.tgz#88562540db7a4a23d6141a4a8fe5584d4b059022"
+ integrity sha512-bTJ50aoC+WDlDGBReWYiObpYvQfMjBNlKztqoNUL0iUkYtwLkBQQeEsTq/I1KyjsKA5tyov6VZaPb8UdD6ci6Q==
+
+"@esbuild/openbsd-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.7.tgz#e19255bdc53fb1c4dc4f3b4266a1813bd948a1ea"
+ integrity sha512-TA9XfJrgzAipFUU895jd9j2SyDh9bbNkK2I0gHcvqb/o84UeQkBpi/XmYX3cO1q/9hZokdcDqQxIi6uLVrikxg==
+
+"@esbuild/openharmony-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.7.tgz#f22324f9ff20c46fcc7f364fe3f8a4a00f6194f3"
+ integrity sha512-5VTtExUrWwHHEUZ/N+rPlHDwVFQ5aME7vRJES8+iQ0xC/bMYckfJ0l2n3yGIfRoXcK/wq4oXSItZAz5wslTKGw==
+
+"@esbuild/sunos-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.7.tgz#72818972a744dfc71d67300e3329c8f26ba7aea8"
+ integrity sha512-umkbn7KTxsexhv2vuuJmj9kggd4AEtL32KodkJgfhNOHMPtQ55RexsaSrMb+0+jp9XL4I4o2y91PZauVN4cH3A==
+
+"@esbuild/win32-arm64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.7.tgz#fe3553fb539b994374a4d2a4ab4f931e351493fd"
+ integrity sha512-j20JQGP/gz8QDgzl5No5Gr4F6hurAZvtkFxAKhiv2X49yi/ih8ECK4Y35YnjlMogSKJk931iNMcd35BtZ4ghfw==
+
+"@esbuild/win32-ia32@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.7.tgz#4b7f9495f635ad697c7ce151b7066134327d4c75"
+ integrity sha512-4qZ6NUfoiiKZfLAXRsvFkA0hoWVM+1y2bSHXHkpdLAs/+r0LgwqYohmfZCi985c6JWHhiXP30mgZawn/XrqAkQ==
+
+"@esbuild/win32-x64@0.25.7":
+ version "0.25.7"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.7.tgz#9105e612d6f760cbc29772dce928710bd92f749c"
+ integrity sha512-FaPsAHTwm+1Gfvn37Eg3E5HIpfR3i6x1AIcla/MkqAIupD4BW3MrSeUqfoTzwwJhk3WE2/KqUn4/eenEJC76VA==
+
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz"
@@ -479,7 +626,7 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@^9.18.0", "@eslint/js@9.31.0":
+"@eslint/js@9.31.0", "@eslint/js@^9.18.0":
version "9.31.0"
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
@@ -791,7 +938,7 @@
jest-haste-map "30.2.0"
slash "^3.0.0"
-"@jest/transform@^29.0.0 || ^30.0.0", "@jest/transform@30.2.0":
+"@jest/transform@30.2.0":
version "30.2.0"
resolved "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz"
integrity sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==
@@ -812,7 +959,7 @@
slash "^3.0.0"
write-file-atomic "^5.0.1"
-"@jest/types@^29.0.0 || ^30.0.0", "@jest/types@30.2.0":
+"@jest/types@30.2.0":
version "30.2.0"
resolved "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz"
integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==
@@ -903,7 +1050,7 @@
clsx "^2.1.0"
prop-types "^15.8.1"
-"@mui/material@^6.3.1", "@mui/material@^6.5.0":
+"@mui/material@^6.3.1":
version "6.5.0"
resolved "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz"
integrity sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==
@@ -1019,6 +1166,15 @@
prop-types "^15.8.1"
react-is "^19.0.0"
+"@napi-rs/wasm-runtime@^0.2.11":
+ version "0.2.12"
+ resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
+ integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
+ dependencies:
+ "@emnapi/core" "^1.4.3"
+ "@emnapi/runtime" "^1.4.3"
+ "@tybys/wasm-util" "^0.10.0"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -1027,7 +1183,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
-"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -1079,11 +1235,106 @@
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz"
integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==
+"@rollup/rollup-android-arm-eabi@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz#8560592f0dcf43b8cb0949af9f1d916205148d12"
+ integrity sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==
+
+"@rollup/rollup-android-arm64@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz#6bfb777bbce998691b6fd3e916b05cd46392d020"
+ integrity sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==
+
+"@rollup/rollup-darwin-arm64@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz#7efce10220293a22e7b7b595d05d8b8400a7bcf3"
+ integrity sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==
+
+"@rollup/rollup-darwin-x64@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz#c617a8ece21050bfbea299c126767d2e70cfa79a"
+ integrity sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==
+
+"@rollup/rollup-freebsd-arm64@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz#5a6af0a9acf82162d2910933649ae24fc0ea3ecb"
+ integrity sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==
+
+"@rollup/rollup-freebsd-x64@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz#ae9709463560196fc275bd0da598668a2e341023"
+ integrity sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz#6ec52661764dbd54c19d6520a403aa385a5c0fbf"
+ integrity sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==
+
+"@rollup/rollup-linux-arm-musleabihf@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz#fd33ba4a43ef8419e96811236493d19436271923"
+ integrity sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==
+
+"@rollup/rollup-linux-arm64-gnu@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz#933b3d99b73c9d7bf4506cab0d5d313c7e74fd2d"
+ integrity sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==
+
+"@rollup/rollup-linux-arm64-musl@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz#dbe9ae24ee9e97b75662fddcb69eb7f23c89280a"
+ integrity sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz#818c5a071eec744436dbcdd76fe9c3c869dc9a8d"
+ integrity sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz#6b8591def27d886fa147fb0340126c7d6682a7e4"
+ integrity sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==
+
+"@rollup/rollup-linux-riscv64-gnu@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz#f1861ac4ee8da64e0b0d23853ff26fe2baa876cf"
+ integrity sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==
+
+"@rollup/rollup-linux-riscv64-musl@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz#320c961401a923b374e358664527b188e374e1ae"
+ integrity sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==
+
+"@rollup/rollup-linux-s390x-gnu@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz#1763eed3362b50b6164d3f0947486c03cc7e616d"
+ integrity sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==
+
"@rollup/rollup-linux-x64-gnu@4.45.1":
version "4.45.1"
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz"
integrity sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==
+"@rollup/rollup-linux-x64-musl@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz#ec30bb48b5fe22a3aaba98072f2d5b7139e1a8eb"
+ integrity sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==
+
+"@rollup/rollup-win32-arm64-msvc@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz#27a6e48d1502e8e4bed96bedfb533738655874f2"
+ integrity sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==
+
+"@rollup/rollup-win32-ia32-msvc@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz#a2fbad3bec20ff879f3fd51720adf33692ca8f3d"
+ integrity sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==
+
+"@rollup/rollup-win32-x64-msvc@4.45.1":
+ version "4.45.1"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz#e5085c6d13da15b4c5133cd2a6bb11f25b6bb77a"
+ integrity sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==
+
"@sinclair/typebox@^0.34.0":
version "0.34.41"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz"
@@ -1113,11 +1364,56 @@
resolved "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
+"@swc/core-darwin-arm64@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.0.tgz#aa8868bbccc8f9857e27beee2a81f161e2dfd5c6"
+ integrity sha512-SkmR9u7MHDu2X8hf7SjZTmsAfQTmel0mi+TJ7AGtufLwGySv6pwQfJ/CIJpcPxYENVqDJAFnDrHaKV8mgA6kxQ==
+
+"@swc/core-darwin-x64@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.13.0.tgz#3f9153b609e0d032d203192e33b8d3d585b64300"
+ integrity sha512-15/SyDjXRtFJ09fYHBXUXrj4tpiSpCkjgsF1z3/sSpHH1POWpQUQzxmFyomPQVZ/SsDqP18WGH09Vph4Qriuiw==
+
+"@swc/core-linux-arm-gnueabihf@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.0.tgz#2b59175952a527b2759ce85ae45a98c9275ce8d4"
+ integrity sha512-AHauVHZQEJI/dCZQg6VYNNQ6HROz8dSOnCSheXzzBw1DGWo77BlcxRP0fF0jaAXM9WNqtCUOY1HiJ9ohkAE61Q==
+
+"@swc/core-linux-arm64-gnu@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.0.tgz#19ac934c4caecd667a7f6ed6b9334719f6b81460"
+ integrity sha512-qyZmBZF7asF6954/x7yn6R7Bzd45KRG05rK2atIF9J3MTa8az7vubP1Q3BWmmss1j8699DELpbuoJucGuhsNXw==
+
+"@swc/core-linux-arm64-musl@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.0.tgz#602315fc4994da757ca03d2e7c9bbf82c00ed89c"
+ integrity sha512-whskQCOUlLQT7MjnronpHmyHegBka5ig9JkQvecbqhWzRfdwN+c2xTJs3kQsWy2Vc2f1hcL3D8hGIwY5TwPxMQ==
+
"@swc/core-linux-x64-gnu@1.13.0":
version "1.13.0"
resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.0.tgz"
integrity sha512-51n4P4nv6rblXyH3zCEktvmR9uSAZ7+zbfeby0sxbj8LS/IKuVd7iCwD5dwMj4CxG9Fs+HgjN73dLQF/OerHhg==
+"@swc/core-linux-x64-musl@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.0.tgz#437ba674afe3d27f4552a1854a8b66c99f97f90c"
+ integrity sha512-VMqelgvnXs27eQyhDf1S2O2MxSdchIH7c1tkxODRtu9eotcAeniNNgqqLjZ5ML0MGeRk/WpbsAY/GWi7eSpiHw==
+
+"@swc/core-win32-arm64-msvc@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.0.tgz#3f4dc1904e0339249acf491924bc758969d20a41"
+ integrity sha512-NLJmseWJngWeENgat+O/WB4ptNxtx2X4OfPnSG5a/A4sxcn2E4jq91OPvbeUQwDkH+ZQWKXmbXFzt7Nn661QYA==
+
+"@swc/core-win32-ia32-msvc@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.0.tgz#f2f0bd9ba3cd1cd937554f71d044d5f014ae4ad4"
+ integrity sha512-UBfwrp0xW37KQGTA08mwrCLIm1ZKy6pXK8IVwou7BvhMgrItRNweTGyUrCnvDLUfyYFuJCmzcEaJ3NudtctD6g==
+
+"@swc/core-win32-x64-msvc@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.0.tgz#b8fc511c21ffed9dad1ab3ebbffa3d8346f68bf0"
+ integrity sha512-BAB1P7Z/y2EENsfsPytPnjIyBVRZN2WULY+s3ozW4QkGmYHde6XXG28n0ABTHhcIOmmR2VzM+uaW1x48laSimw==
+
"@swc/core@^1.12.11":
version "1.13.0"
resolved "https://registry.npmjs.org/@swc/core/-/core-1.13.0.tgz"
@@ -1149,7 +1445,7 @@
dependencies:
"@swc/counter" "^0.1.3"
-"@testing-library/dom@^10.0.0", "@testing-library/dom@^10.4.1":
+"@testing-library/dom@^10.4.1":
version "10.4.1"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz"
integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==
@@ -1191,6 +1487,13 @@
minimatch "^10.0.1"
path-browserify "^1.0.1"
+"@tybys/wasm-util@^0.10.0":
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
+ integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
+ dependencies:
+ tslib "^2.4.0"
+
"@types/aria-query@^5.0.1":
version "5.0.4"
resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz"
@@ -1229,7 +1532,7 @@
dependencies:
"@babel/types" "^7.28.2"
-"@types/estree@^1.0.6", "@types/estree@1.0.8":
+"@types/estree@1.0.8", "@types/estree@^1.0.6":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -1280,7 +1583,7 @@
resolved "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz"
integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==
-"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0":
+"@types/node@*":
version "24.6.2"
resolved "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz"
integrity sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==
@@ -1297,7 +1600,7 @@
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz"
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
-"@types/react-dom@^18", "@types/react-dom@^18.0.0 || ^19.0.0":
+"@types/react-dom@^18":
version "18.3.7"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz"
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
@@ -1307,7 +1610,7 @@
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
-"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18", "@types/react@^18.0.0", "@types/react@^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19":
+"@types/react@^18":
version "18.3.23"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz"
integrity sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==
@@ -1357,7 +1660,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
-"@typescript-eslint/parser@^8.37.0", "@typescript-eslint/parser@8.37.0":
+"@typescript-eslint/parser@8.37.0":
version "8.37.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz"
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
@@ -1385,7 +1688,7 @@
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
-"@typescript-eslint/tsconfig-utils@^8.37.0", "@typescript-eslint/tsconfig-utils@8.37.0":
+"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
version "8.37.0"
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz"
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
@@ -1401,7 +1704,7 @@
debug "^4.3.4"
ts-api-utils "^2.1.0"
-"@typescript-eslint/types@^8.37.0", "@typescript-eslint/types@8.37.0":
+"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
version "8.37.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz"
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
@@ -1445,11 +1748,103 @@
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
+"@unrs/resolver-binding-android-arm-eabi@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81"
+ integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==
+
+"@unrs/resolver-binding-android-arm64@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f"
+ integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==
+
+"@unrs/resolver-binding-darwin-arm64@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf"
+ integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==
+
+"@unrs/resolver-binding-darwin-x64@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc"
+ integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==
+
+"@unrs/resolver-binding-freebsd-x64@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b"
+ integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==
+
+"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a"
+ integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==
+
+"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3"
+ integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==
+
+"@unrs/resolver-binding-linux-arm64-gnu@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d"
+ integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==
+
+"@unrs/resolver-binding-linux-arm64-musl@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0"
+ integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==
+
+"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44"
+ integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==
+
+"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9"
+ integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==
+
+"@unrs/resolver-binding-linux-riscv64-musl@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165"
+ integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==
+
+"@unrs/resolver-binding-linux-s390x-gnu@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94"
+ integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==
+
"@unrs/resolver-binding-linux-x64-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz"
integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==
+"@unrs/resolver-binding-linux-x64-musl@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6"
+ integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==
+
+"@unrs/resolver-binding-wasm32-wasi@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d"
+ integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==
+ dependencies:
+ "@napi-rs/wasm-runtime" "^0.2.11"
+
+"@unrs/resolver-binding-win32-arm64-msvc@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35"
+ integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==
+
+"@unrs/resolver-binding-win32-ia32-msvc@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6"
+ integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==
+
+"@unrs/resolver-binding-win32-x64-msvc@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777"
+ integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==
+
"@vitejs/plugin-react-swc@^3.7.2":
version "3.11.0"
resolved "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz"
@@ -1463,7 +1858,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
-"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
+acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -1507,12 +1902,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
-ansi-styles@^5.0.0:
- version "5.2.0"
- resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
- integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
-
-ansi-styles@^5.2.0:
+ansi-styles@^5.0.0, ansi-styles@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
@@ -1542,7 +1932,7 @@ argparse@^2.0.1:
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-aria-query@^5.0.0, aria-query@5.3.0:
+aria-query@5.3.0, aria-query@^5.0.0:
version "5.3.0"
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz"
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
@@ -1651,7 +2041,7 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
-"babel-jest@^29.0.0 || ^30.0.0", babel-jest@30.2.0:
+babel-jest@30.2.0:
version "30.2.0"
resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz"
integrity sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==
@@ -1747,7 +2137,7 @@ braces@^3.0.3:
dependencies:
fill-range "^7.1.1"
-browserslist@^4.24.0, browserslist@^4.24.4, "browserslist@>= 4.21.0":
+browserslist@^4.24.0, browserslist@^4.24.4:
version "4.25.1"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz"
integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==
@@ -1984,7 +2374,7 @@ date-fns@^4.1.0:
resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz"
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
-debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4:
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
version "4.4.1"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
@@ -2324,7 +2714,7 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
-"eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.18.0:
+eslint@^9.18.0:
version "9.31.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz"
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
@@ -2423,7 +2813,7 @@ exit-x@^0.2.2:
resolved "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz"
integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==
-expect@^30.0.0, expect@30.2.0:
+expect@30.2.0, expect@^30.0.0:
version "30.2.0"
resolved "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz"
integrity sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==
@@ -2451,7 +2841,7 @@ fast-glob@^3.3.2, fast-glob@^3.3.3:
merge2 "^1.3.0"
micromatch "^4.0.8"
-fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x:
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@@ -2499,15 +2889,7 @@ find-root@^1.1.0:
resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz"
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
-find-up@^4.0.0:
- version "4.1.0"
- resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-find-up@^4.1.0:
+find-up@^4.0.0, find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
@@ -2561,6 +2943,16 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+fsevents@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+fsevents@^2.3.3, fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -3375,7 +3767,7 @@ jest-resolve-dependencies@30.2.0:
jest-regex-util "30.0.1"
jest-snapshot "30.2.0"
-jest-resolve@*, jest-resolve@30.2.0:
+jest-resolve@30.2.0:
version "30.2.0"
resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz"
integrity sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==
@@ -3472,7 +3864,7 @@ jest-snapshot@30.2.0:
semver "^7.7.2"
synckit "^0.11.8"
-"jest-util@^29.0.0 || ^30.0.0", jest-util@30.2.0:
+jest-util@30.2.0:
version "30.2.0"
resolved "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz"
integrity sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==
@@ -3521,7 +3913,7 @@ jest-worker@30.2.0:
merge-stream "^2.0.0"
supports-color "^8.1.1"
-"jest@^29.0.0 || ^30.0.0", jest@^30.2.0:
+jest@^30.2.0:
version "30.2.0"
resolved "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz"
integrity sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==
@@ -3551,7 +3943,7 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
-jsdom@*, jsdom@^26.1.0:
+jsdom@^26.1.0:
version "26.1.0"
resolved "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz"
integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==
@@ -3673,12 +4065,7 @@ loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
-lru-cache@^10.2.0:
- version "10.4.3"
- resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz"
- integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
-
-lru-cache@^10.4.3:
+lru-cache@^10.2.0, lru-cache@^10.4.3:
version "10.4.3"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
@@ -4027,7 +4414,7 @@ path-type@^4.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-picocolors@^1.1.1, picocolors@1.1.1:
+picocolors@1.1.1, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -4037,7 +4424,7 @@ picomatch@^2.0.4, picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-"picomatch@^3 || ^4", picomatch@^4.0.2:
+picomatch@^4.0.2:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
@@ -4078,7 +4465,7 @@ postcss-value-parser@^4.2.0:
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-postcss@^8.1.0, postcss@^8.4.49, postcss@^8.5.3:
+postcss@^8.4.49, postcss@^8.5.3:
version "8.5.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
@@ -4104,21 +4491,12 @@ prettier-plugin-react@^0.0.1:
dependencies:
ts-morph "^26.0.0"
-prettier@^3.4.2, prettier@>=2.0:
+prettier@^3.4.2:
version "3.6.2"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
-pretty-format@^27.0.2:
- version "27.5.1"
- resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz"
- integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
- dependencies:
- ansi-regex "^5.0.1"
- ansi-styles "^5.0.0"
- react-is "^17.0.1"
-
-pretty-format@^30.0.0:
+pretty-format@30.2.0, pretty-format@^30.0.0:
version "30.2.0"
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz"
integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==
@@ -4127,14 +4505,14 @@ pretty-format@^30.0.0:
ansi-styles "^5.2.0"
react-is "^18.3.1"
-pretty-format@30.2.0:
- version "30.2.0"
- resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz"
- integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==
+pretty-format@^27.0.2:
+ version "27.5.1"
+ resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz"
+ integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
dependencies:
- "@jest/schemas" "30.0.5"
- ansi-styles "^5.2.0"
- react-is "^18.3.1"
+ ansi-regex "^5.0.1"
+ ansi-styles "^5.0.0"
+ react-is "^17.0.1"
prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
@@ -4160,19 +4538,14 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-"react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19.1.0, react-dom@>=16.6.0, react-dom@>=16.8.0, react-dom@>=18:
+react-dom@^19.1.0:
version "19.1.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz"
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
dependencies:
scheduler "^0.26.0"
-react-is@^16.13.1:
- version "16.13.1"
- resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
- integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-
-react-is@^16.7.0:
+react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -4192,7 +4565,7 @@ react-is@^19.0.0:
resolved "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz"
integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==
-"react-redux@^7.2.1 || ^8.1.3 || ^9.0.0", react-redux@^9.1.2:
+react-redux@^9.1.2:
version "9.2.0"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz"
integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
@@ -4225,7 +4598,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
-"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.9.0 || ^17.0.0 || ^18 || ^19", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19.1.0, react@>=16.6.0, react@>=16.8.0, react@>=18:
+react@^19.1.0:
version "19.1.0"
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
@@ -4243,7 +4616,7 @@ redux-thunk@^3.1.0:
resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz"
integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
-redux@^5.0.0, redux@^5.0.1:
+redux@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
@@ -4415,22 +4788,7 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.5.3:
- version "7.7.2"
- resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
-
-semver@^7.5.4:
- version "7.7.2"
- resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
-
-semver@^7.6.0:
- version "7.7.2"
- resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
-
-semver@^7.7.2:
+semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.2:
version "7.7.2"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
@@ -4556,12 +4914,7 @@ source-map@^0.5.7:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
-source-map@^0.6.0:
- version "0.6.1"
- resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
- integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-source-map@^0.6.1:
+source-map@^0.6.0, source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -4603,16 +4956,7 @@ string-length@^4.0.2:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
-string-width@^4.1.0, string-width@^4.2.0:
- version "4.2.3"
- resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.2.3:
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -4851,6 +5195,11 @@ ts-morph@^26.0.0:
"@ts-morph/common" "~0.27.0"
code-block-writer "^13.0.3"
+tslib@^2.4.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -4928,7 +5277,7 @@ typescript-eslint@^8.19.1:
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
-typescript@^5.7.3, typescript@>=2.9, "typescript@>=4.3 <6", typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0":
+typescript@^5.7.3:
version "5.8.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
@@ -5014,7 +5363,7 @@ vite-plugin-package-version@^1.1.0:
resolved "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz"
integrity sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==
-"vite@^4 || ^5 || ^6 || ^7", vite@^6.2.7, vite@>=2.0.0-beta.69:
+vite@^6.2.7:
version "6.3.6"
resolved "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz"
integrity sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==
@@ -5207,11 +5556,6 @@ yaml@^1.10.0:
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-yaml@^2.4.2:
- version "2.8.1"
- resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
- integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
-
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"