diff --git a/.gitignore b/.gitignore index 11be5249..f37a2e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ server/manifest.go assets/i18n/translate.*.json +public/jaas + # Jetbrains .idea/ diff --git a/go.mod b/go.mod index 1ad87548..080b3667 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/mattermost/mattermost-plugin-jitsi go 1.21 require ( - github.com/cristalhq/jwt/v2 v2.0.0 + github.com/cristalhq/jwt/v3 v3.1.0 github.com/google/uuid v1.5.0 + github.com/gorilla/mux v1.8.1 github.com/mattermost/mattermost/server/public v0.0.12 github.com/nicksnyder/go-i18n/v2 v2.3.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 8443592d..5669225f 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cristalhq/jwt/v2 v2.0.0 h1:CxleHxkZQQ5J0siUQ2gwZrhAysmh8Ddh/R06AzCiYao= -github.com/cristalhq/jwt/v2 v2.0.0/go.mod h1:nQT19GqJbrWubmI+ULE8PYsR1GCbwI5hAg1nG+9AbTg= +github.com/cristalhq/jwt/v3 v3.1.0 h1:iLeL9VzB0SCtjCy9Kg53rMwTcrNm+GHyVcz2eUujz6s= +github.com/cristalhq/jwt/v3 v3.1.0/go.mod h1:XOnIXst8ozq/esy5N1XOlSyQqBd+84fxJ99FK+1jgL8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -69,6 +69,8 @@ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= diff --git a/plugin.json b/plugin.json index f3c80ca3..ebcb2381 100644 --- a/plugin.json +++ b/plugin.json @@ -19,12 +19,21 @@ "settings_schema": { "settings": [ { - "key": "JitsiURL", - "display_name": "Jitsi Server URL:", - "type": "text", - "help_text": "The URL for your Jitsi server, for example https://jitsi.example.com. Defaults to https://meet.jit.si, which is the public server provided by Jitsi.", - "placeholder": "https://meet.jit.si", - "default": "https://meet.jit.si" + "key": "ServerType", + "display_name": "Server:", + "type": "radio", + "help_text": "Select the type of jitsi server you want to use.", + "default": "jitsi", + "options": [ + { + "display_name": "Jitsi", + "value": "jitsi" + }, + { + "display_name": "JaaS", + "value": "jaas" + } + ] }, { "key": "JitsiEmbedded", @@ -33,6 +42,21 @@ "help_text": "(Experimental) When true, Jitsi video is embedded as a floating window inside Mattermost by default. Users can override this setting with '/jitsi settings'." }, { + "key": "JitsiCompatibilityMode", + "display_name": "Enable Compatibility Mode:", + "type": "bool", + "help_text": "(Insecure) If your Jitsi server is not compatible with this plugin, include the JavaScript API hosted on your Jitsi server directly in Mattermost instead of the default API version provided by the plugin. **WARNING:** Enabling this setting can compromise the security of your Mattermost system, if your Jitsi server is not fully trusted and allows direct modification of program files. Use with caution.", + "default": false + }, + { + "key": "JitsiURL", + "display_name": "Jitsi Server URL:", + "type": "text", + "help_text": "The URL for your Jitsi server, for example https://jitsi.example.com. Defaults to https://meet.jit.si, which is the public server provided by Jitsi.", + "placeholder": "https://meet.jit.si", + "default": "https://meet.jit.si" + }, + { "key": "JitsiPrejoinPage", "display_name": "Show pre-join page:", "type": "bool", @@ -89,11 +113,22 @@ "default": 30 }, { - "key": "JitsiCompatibilityMode", - "display_name": "Enable Compatibility Mode:", - "type": "bool", - "help_text": "(Insecure) If your Jitsi server is not compatible with this plugin, include the JavaScript API hosted on your Jitsi server directly in Mattermost instead of the default API version provided by the plugin. **WARNING:** Enabling this setting can compromise the security of your Mattermost system, if your Jitsi server is not fully trusted and allows direct modification of program files. Use with caution.", - "default": false + "key": "JaaSAppID", + "display_name": "AppID for JaaS Authentication:", + "type": "text", + "help_text": "Specify your JaaS AppID. You can get the AppID from https://jaas.8x8.vc/#/apikeys." + }, + { + "key": "JaaSApiKey", + "display_name": "Api key ID for JaaS Authentication:", + "type": "text", + "help_text": "Specify your JaaS Api key ID. You can get the Api key ID from https://jaas.8x8.vc/#/apikeys." + }, + { + "key": "JaaSPrivateKey", + "display_name": "RSA Private key for JaaS Authentication:", + "type": "longtext", + "help_text": "Specify your JaaS private key. You can get the private key by generating an API key pair from https://jaas.8x8.vc/#/apikeys OR from https://jaas.8x8.vc/#/start-guide in the Api Key section. (NOTE: While generating the key manually make sure to set the size to be 2048 and not 4096)." } ] } diff --git a/server/api.go b/server/api.go index 54519efe..87a0c8d5 100644 --- a/server/api.go +++ b/server/api.go @@ -3,18 +3,21 @@ package main import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "os" "path/filepath" "sync" + "github.com/gorilla/mux" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" ) const externalAPICacheTTL = 3600000 +const mattermostUserIDHeader = "Mattermost-User-Id" var externalAPICache []byte var externalAPILastUpdate int64 @@ -37,42 +40,115 @@ type StartMeetingFromAction struct { } `json:"context"` } -func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) { - switch path := r.URL.Path; path { - case "/api/v1/meetings/enrich": - p.handleEnrichMeetingJwt(w, r) - case "/api/v1/meetings": - p.handleStartMeeting(w, r) - case "/api/v1/config": - p.handleConfig(w, r) - case "/jitsi_meet_external_api.js": - p.handleExternalAPIjs(w, r) - default: - http.NotFound(w, r) +type JaaSSettingsFromAction struct { + Jwt string `json:"jaasJwt"` + Path string `json:"jaasPath"` +} + +func (p *Plugin) InitAPI() *mux.Router { + r := mux.NewRouter() + + apiRouter := r.PathPrefix("/api/v1").Subrouter() + + r.HandleFunc("/jitsi_meet_external_api.js", p.handleExternalAPIjs) + apiRouter.HandleFunc("/meetings/enrich", p.checkAuth(p.handleEnrichMeetingJwt)).Methods(http.MethodPost) + apiRouter.HandleFunc("/meetings", p.checkAuth(p.handleStartMeeting)).Methods(http.MethodPost) + apiRouter.HandleFunc("/config", p.checkAuth(p.handleConfig)).Methods(http.MethodPost) + apiRouter.HandleFunc("/meetings/jaas/settings", p.handleJaaSSettings) + r.HandleFunc("{anything:.*}", http.NotFound) + + return r +} + +func (p *Plugin) checkAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get(mattermostUserIDHeader) + if mattermostUserID == "" { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + handler(w, r) } } -func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-Id") +func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) { + p.router.ServeHTTP(w, r) +} - if userID == "" { - http.Error(w, "Not authorized", http.StatusUnauthorized) +func (p *Plugin) handleJaaSSettings(w http.ResponseWriter, r *http.Request) { + if !p.getConfiguration().UseJaaS { + errString := "Error JaaS requested while disabled" + mlog.Error(errString) + http.Error(w, errString, http.StatusBadRequest) return } + if err := p.getConfiguration().IsValid(); err != nil { + mlog.Error("Invalid plugin configuration", mlog.Err(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var jaasSettingsFromAction JaaSSettingsFromAction + if err := json.NewDecoder(r.Body).Decode(&jaasSettingsFromAction); err != nil { + mlog.Debug("Unable to decode the request content as start meeting request or start meeting action") + http.Error(w, "Unable to decode your request", http.StatusBadRequest) + return + } + r.Body.Close() + + var user *model.User + userID := r.Header.Get(mattermostUserIDHeader) + if userID != "" { + // Handle moderator + userResult, appErr := p.API.GetUser(userID) + if appErr != nil { + mlog.Debug("Unable to get the user", mlog.Err(appErr)) + http.Error(w, "You are forbidden to get the user", http.StatusForbidden) + return + } + user = userResult + } + + jaasSettings, err := p.getJaaSSettings(jaasSettingsFromAction.Jwt, jaasSettingsFromAction.Path, user) + if err != nil { + mlog.Error("Error getting JaaSSettings", mlog.Err(err)) + http.Error(w, "Invalid JaaS settings", http.StatusBadRequest) + return + } + + settingsJSON, err := json.Marshal(jaasSettings) + if err != nil { + mlog.Error("Error marshaling the JaaSSettings to json", mlog.Err(err)) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(settingsJSON) + if err != nil { + mlog.Warn("Unable to write response body", mlog.String("handler", "handleJaaSSettings"), mlog.Err(err)) + } +} + +func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get(mattermostUserIDHeader) + config, err := p.getUserConfig(userID) if err != nil { mlog.Error("Error getting user config", mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(w, "Error getting user config", http.StatusInternalServerError) return } b, err := json.Marshal(config) if err != nil { mlog.Error("Error marshaling the Config to json", mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(w, "Error marshaling the Config to json", http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/json") _, err = w.Write(b) if err != nil { @@ -82,27 +158,31 @@ func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleExternalAPIjs(w http.ResponseWriter, r *http.Request) { if p.getConfiguration().JitsiCompatibilityMode { + if p.getConfiguration().UseJaaS { + p.proxyExternalAPIjsJaaS(w, r) + return + } p.proxyExternalAPIjs(w, r) return } bundlePath, err := p.API.GetBundlePath() if err != nil { - mlog.Error("Filed to get the bundle path") - http.Error(w, "Internal error", http.StatusInternalServerError) + mlog.Error("Failed to get the bundle path") + http.Error(w, "Failed to get the bundle path", http.StatusInternalServerError) return } externalAPIPath := filepath.Join(bundlePath, "assets", "external_api.js") externalAPIFile, err := os.Open(externalAPIPath) if err != nil { mlog.Error("Error opening file", mlog.String("path", externalAPIPath), mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(w, "Error opening file", http.StatusInternalServerError) return } code, err := io.ReadAll(externalAPIFile) if err != nil { mlog.Error("Error reading file content", mlog.String("path", externalAPIPath), mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(w, "Error reading file content", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/javascript") @@ -112,6 +192,37 @@ func (p *Plugin) handleExternalAPIjs(w http.ResponseWriter, r *http.Request) { } } +func (p *Plugin) proxyExternalAPIjsJaaS(w http.ResponseWriter, _ *http.Request) { + externalAPICacheMutex.Lock() + defer externalAPICacheMutex.Unlock() + + if externalAPICache != nil && externalAPILastUpdate > (model.GetMillis()-externalAPICacheTTL) { + w.Header().Set("Content-Type", "application/javascript") + _, _ = w.Write(externalAPICache) + return + } + resp, err := http.Get(fmt.Sprintf("%s/libs/external_api.min.js", p.getConfiguration().Get8x8vcURL())) + if err != nil { + mlog.Error("Error getting the external_api.min.js file from your 8x8", mlog.Err(err)) + http.Error(w, "Error getting the external_api.min.js file from your 8x8", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + mlog.Error("Error reading the content", mlog.String("url", fmt.Sprintf("%s/external_api.min.js", p.getConfiguration().Get8x8vcURL())), mlog.Err(err)) + http.Error(w, "Error reading the content", http.StatusInternalServerError) + return + } + externalAPICache = body + externalAPILastUpdate = model.GetMillis() + w.Header().Set("Content-Type", "application/javascript") + _, err = w.Write(body) + if err != nil { + mlog.Warn("Unable to write response body", mlog.String("handler", "proxyExternalAPIjsJaaS"), mlog.Err(err)) + } +} + func (p *Plugin) proxyExternalAPIjs(w http.ResponseWriter, _ *http.Request) { externalAPICacheMutex.Lock() defer externalAPICacheMutex.Unlock() @@ -124,14 +235,14 @@ func (p *Plugin) proxyExternalAPIjs(w http.ResponseWriter, _ *http.Request) { resp, err := http.Get(p.getConfiguration().GetJitsiURL() + "/external_api.js") if err != nil { mlog.Error("Error getting the external_api.js file from your Jitsi instance, please verify your JitsiURL setting", mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(w, "Error getting the external_api.js file from your Jitsi instance, please verify your JitsiURL setting", http.StatusInternalServerError) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - mlog.Error("Error getting reading the content", mlog.String("url", p.getConfiguration().GetJitsiURL()+"/external_api.js"), mlog.Err(err)) - http.Error(w, "Internal error", http.StatusInternalServerError) + mlog.Error("Error reading external_api.js file", mlog.String("url", p.getConfiguration().GetJitsiURL()+"/external_api.js"), mlog.Err(err)) + http.Error(w, "Error reading external_api.js file", http.StatusInternalServerError) return } externalAPICache = body @@ -150,16 +261,11 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { return } - userID := r.Header.Get("Mattermost-User-Id") - - if userID == "" { - http.Error(w, "Not authorized", http.StatusUnauthorized) - return - } + userID := r.Header.Get(mattermostUserIDHeader) user, appErr := p.API.GetUser(userID) if appErr != nil { - mlog.Debug("Unable to the user", mlog.Err(appErr)) + mlog.Debug("Unable to get the user", mlog.Err(appErr)) http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -222,7 +328,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { var meetingID string if userConfig.NamingScheme == jitsiNameSchemeAsk && action.PostId != "" { - meetingID, err = p.startMeeting(user, channel, action.Context.MeetingID, action.Context.MeetingTopic, action.Context.Personal, "") + meetingID, err = p.startMeeting(user, channel, action.Context.MeetingID, action.Context.MeetingTopic, "") if err != nil { mlog.Error("Error starting a new meeting from ask response", mlog.Err(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -230,7 +336,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { } p.deleteEphemeralPost(action.UserId, action.PostId) } else { - meetingID, err = p.startMeeting(user, channel, "", req.Topic, req.Personal, action.Context.RootID) + meetingID, err = p.startMeeting(user, channel, "", req.Topic, action.Context.RootID) if err != nil { mlog.Error("Error starting a new meeting", mlog.Err(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -244,6 +350,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal error", http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/json") _, err = w.Write(b) if err != nil { @@ -258,11 +365,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request) return } - userID := r.Header.Get("Mattermost-User-Id") - if userID == "" { - http.Error(w, "Not authorized", http.StatusUnauthorized) - return - } + userID := r.Header.Get(mattermostUserIDHeader) var req EnrichMeetingJwtRequest @@ -278,7 +381,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request) http.Error(w, err.Error(), err.StatusCode) } - JWTMeeting := p.getConfiguration().JitsiJWT + JWTMeeting := p.getConfiguration().JitsiJWT || p.getConfiguration().UseJaaS if !JWTMeeting { http.Error(w, "Not authorized", http.StatusUnauthorized) @@ -298,6 +401,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request) http.Error(w, "Internal error", http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/json") _, err2 = w.Write(b) if err2 != nil { diff --git a/server/command.go b/server/command.go index 25f9a107..ed465cb6 100644 --- a/server/command.go +++ b/server/command.go @@ -162,7 +162,7 @@ func (p *Plugin) executeStartMeetingCommand(_ *plugin.Context, args *model.Comma return startMeetingError(args.ChannelId, fmt.Sprintf("startMeeting() threw error: %s", appErr)) } } else { - if _, err := p.startMeeting(user, channel, "", input, false, args.RootId); err != nil { + if _, err := p.startMeeting(user, channel, "", input, args.RootId); err != nil { return startMeetingError(args.ChannelId, fmt.Sprintf("startMeeting() threw error: %s", appErr)) } } diff --git a/server/configuration.go b/server/configuration.go index c3b03e39..a217d155 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry" + "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/pkg/errors" ) @@ -34,10 +35,17 @@ type configuration struct { JitsiJWT bool JitsiEmbedded bool JitsiCompatibilityMode bool + JaaSAppID string + JaaSApiKey string + JaaSPrivateKey string + UseJaaS bool + ServerType string JitsiPrejoinPage bool } +const JaaSServerType = "jaas" const publicJitsiServerURL = "https://meet.jit.si" +const public8x8vcURL = "https://8x8.vc" // GetJitsiURL return the currently configured JitsiURL or the URL from the // public servers provided by Jitsi. @@ -52,6 +60,10 @@ func (c *configuration) GetDefaultJitsiURL() string { return publicJitsiServerURL } +func (c *configuration) Get8x8vcURL() string { + return public8x8vcURL +} + // Clone shallow copies the configuration. Your implementation may require a deep copy if // your configuration has reference types. func (c *configuration) Clone() *configuration { @@ -68,7 +80,7 @@ func (c *configuration) IsValid() error { } } - if c.JitsiJWT { + if c.JitsiJWT && c.ServerType != JaaSServerType { if len(c.JitsiAppID) == 0 { return fmt.Errorf("error no Jitsi app ID was provided to use with JWT") } @@ -80,6 +92,20 @@ func (c *configuration) IsValid() error { } } + if c.ServerType == JaaSServerType { + if len(c.JaaSApiKey) == 0 { + mlog.Error("error no JaaS Api Key was provided for JaaS") + } + + if len(c.JaaSAppID) == 0 { + mlog.Error("error no JaaS AppID was provided for JaaS") + } + + if len(c.JaaSPrivateKey) == 0 { + mlog.Error("error no JaaS Private Key was provided for JaaS") + } + } + return nil } @@ -91,7 +117,8 @@ func (p *Plugin) getConfiguration() *configuration { defer p.configurationLock.RUnlock() if p.configuration == nil { - return &configuration{} + newConfiguration := configuration{} + return &newConfiguration } return p.configuration @@ -122,13 +149,17 @@ func (p *Plugin) setConfiguration(configuration *configuration) { } p.API.PublishWebSocketEvent(configChangeEvent, nil, &model.WebsocketBroadcast{}) + if configuration.ServerType == JaaSServerType { + configuration.UseJaaS = true + } else { + configuration.UseJaaS = false + } p.configuration = configuration } // OnConfigurationChange is invoked when configuration changes may have been made. func (p *Plugin) OnConfigurationChange() error { var configuration = new(configuration) - // Load the public configuration fields from the Mattermost server configuration. if err := p.API.LoadPluginConfiguration(configuration); err != nil { return errors.Wrap(err, "failed to load plugin configuration") @@ -137,7 +168,6 @@ func (p *Plugin) OnConfigurationChange() error { p.tracker = telemetry.NewTracker(p.telemetryClient, p.API.GetDiagnosticId(), p.API.GetServerVersion(), manifest.Id, manifest.Version, "jitsi", telemetry.NewTrackerConfig(p.API.GetConfig()), logger.New(p.API)) p.setConfiguration(configuration) - return nil } diff --git a/server/model.go b/server/model.go new file mode 100644 index 00000000..f5b09c58 --- /dev/null +++ b/server/model.go @@ -0,0 +1,70 @@ +package main + +import ( + "github.com/cristalhq/jwt/v3" +) + +type UserConfig struct { + NamingScheme string `json:"naming_scheme"` + Embedded bool `json:"embedded"` + UseJaas bool `json:"use_jaas"` + ShowPrejoinPage bool `json:"show_prejoin_page"` +} + +type User struct { + Avatar string `json:"avatar"` + Name string `json:"name"` + Email string `json:"email"` + ID string `json:"id"` +} + +type Context struct { + User User `json:"user"` + Group string `json:"group"` +} + +type EnrichMeetingJwtRequest struct { + Jwt string `json:"jwt"` +} + +// Claims extends cristalhq/jwt standard claims to add jitsi-web-token specific fields +type Claims struct { + jwt.StandardClaims + Context Context `json:"context"` + Room string `json:"room,omitempty"` +} + +type JaaSUser struct { + Avatar string `json:"avatar"` + Name string `json:"name"` + Email string `json:"email"` + ID string `json:"id,omitempty"` + Moderator string `json:"moderator,omitempty"` +} + +type JaaSFeatures struct { + LiveStreaming string `json:"livestreaming"` + Recording string `json:"recording"` + OutboundCall string `json:"outbound-call"` + Transcription string `json:"transcription"` +} + +type JaaSContext struct { + User JaaSUser `json:"user"` + Features JaaSFeatures `json:"features"` +} + +type JaaSClaims struct { + Context JaaSContext `json:"context"` + Audience string `json:"aud,omitempty"` + Subject string `json:"sub,omitempty"` + Issuer string `json:"iss,omitempty"` + Room string `json:"room,omitempty"` + Exp int64 `json:"exp,omitempty"` + Nbf int64 `json:"nbf,omitempty"` +} + +type JaaSSettings struct { + Jwt string `json:"jaasJwt"` + Room string `json:"jaasRoom"` +} diff --git a/server/plugin.go b/server/plugin.go index 6dc50046..68730695 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -1,7 +1,10 @@ package main import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "net/url" "path/filepath" @@ -10,7 +13,9 @@ import ( "sync" "time" - "github.com/cristalhq/jwt/v2" + "github.com/cristalhq/jwt/v3" + "github.com/google/uuid" + "github.com/gorilla/mux" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/pluginapi" @@ -26,11 +31,13 @@ const jitsiNameSchemeUUID = "uuid" const jitsiNameSchemeMattermost = "mattermost" const configChangeEvent = "config_update" -type UserConfig struct { - NamingScheme string `json:"naming_scheme"` - Embedded bool `json:"embedded"` - ShowPrejoinPage bool `json:"show_prejoin_page"` -} +const jaasAudienceClaim = "jitsi" +const jaasIssuerClaim = "chat" +const rsaPrivateKey = "RSA PRIVATE KEY" +const privateKey = "PRIVATE KEY" +const DefaultValidityOfMeetingLinkInMinutes = 120 +const typeUser = "user" +const typeGuest = "guest" type Plugin struct { plugin.MattermostPlugin @@ -50,6 +57,8 @@ type Plugin struct { b *i18n.Bundle botID string + + router *mux.Router } func (p *Plugin) OnActivate() error { @@ -92,33 +101,27 @@ func (p *Plugin) OnActivate() error { p.telemetryClient, err = telemetry.NewRudderClient() if err != nil { - p.API.LogWarn("telemetry client not started", "error", err.Error()) + p.API.LogWarn("Telemetry client not started", "error", err.Error()) } + p.router = p.InitAPI() return nil } -type User struct { - Avatar string `json:"avatar"` - Name string `json:"name"` - Email string `json:"email"` - ID string `json:"id"` -} - -type Context struct { - User User `json:"user"` - Group string `json:"group"` -} +func getJwtClaims(jaasToken string) (*JaaSClaims, error) { + newToken, err := jwt.ParseString(jaasToken) + if err != nil { + mlog.Error("Error parsing jaas jwt", mlog.Err(err)) + return nil, err + } -type EnrichMeetingJwtRequest struct { - Jwt string `json:"jwt"` -} + var claims JaaSClaims + if err = json.Unmarshal(newToken.RawClaims(), &claims); err != nil { + mlog.Error("Error unmarshalling claims for jaas jwt", mlog.Err(err)) + return nil, err + } -// Claims extents cristalhq/jwt standard claims to add jitsi-web-token specific fields -type Claims struct { - jwt.StandardClaims - Context Context `json:"context"` - Room string `json:"room,omitempty"` + return &claims, nil } func verifyJwt(secret string, jwtToken string) (*Claims, error) { @@ -146,7 +149,7 @@ func signClaims(secret string, claims *Claims) (string, error) { signer, err := jwt.NewSignerHS(jwt.HS256, []byte(secret)) if err != nil { mlog.Error("Error generating new HS256 signer", mlog.Err(err)) - return "", errors.New("internal error") + return "", errors.New("error generating new HS256 signer") } builder := jwt.NewBuilder(signer) token, err := builder.Build(claims) @@ -156,6 +159,54 @@ func signClaims(secret string, claims *Claims) (string, error) { return string(token.Raw()), nil } +func signClaimsJaaS(apiKeyJaaS string, privateKeyJaaS string, claimsJaaS *JaaSClaims) (string, error) { + var err error + privateKeyJaaSBytes := []byte(privateKeyJaaS) + privPem, _ := pem.Decode(privateKeyJaaSBytes) + if privPem == nil { + mlog.Error("Error decoding specified JaaS private key") + return "", errors.New("error decoding specified JaaS private key") + } + + privPemBytes := privPem.Bytes + var parsedKey interface{} + switch privPem.Type { + case rsaPrivateKey: + parsedKey, err = x509.ParsePKCS1PrivateKey(privPemBytes) + case privateKey: + parsedKey, err = x509.ParsePKCS8PrivateKey(privPemBytes) + default: + return "", errors.New("invalid private key") + } + + if err != nil { + mlog.Error("Error parsing JaaS private key", mlog.Err(err)) + return "", errors.Wrap(err, "Error parsing JaaS private key") + } + + success := false + var privateKey *rsa.PrivateKey + privateKey, success = parsedKey.(*rsa.PrivateKey) + if !success { + mlog.Error("Error converting JaaS private Key") + return "", errors.New("error converting JaaS private Key") + } + + signer, err := jwt.NewSignerRS(jwt.RS256, privateKey) + if err != nil { + return "", err + } + + builder := jwt.NewBuilder(signer, jwt.WithKeyID(apiKeyJaaS)) + + token, err := builder.Build(claimsJaaS) + if err != nil { + return "", err + } + + return string(token.Raw()), nil +} + func (p *Plugin) trackMeeting(_ *model.CommandArgs) { // disables tracking if the user is not using the default jitsi url isNotDefaultJitsiURL := p.isNotDefaultJitsiURL() @@ -168,8 +219,8 @@ func (p *Plugin) trackMeeting(_ *model.CommandArgs) { } func (p *Plugin) isNotDefaultJitsiURL() bool { - currentURL := p.configuration.GetJitsiURL() - defaultURL := p.configuration.GetDefaultJitsiURL() + currentURL := p.getConfiguration().GetJitsiURL() + defaultURL := p.getConfiguration().GetDefaultJitsiURL() return currentURL != defaultURL } @@ -178,9 +229,14 @@ func (p *Plugin) deleteEphemeralPost(userID, postID string) { } func (p *Plugin) updateJwtUserInfo(jwtToken string, user *model.User) (string, error) { - secret := p.getConfiguration().JitsiAppSecret sanitizedUser := user.DeepCopy() + if p.getConfiguration().UseJaaS { + return p.getSignClaimsJaaS(jwtToken, sanitizedUser) + } + + secret := p.getConfiguration().JitsiAppSecret + claims, err := verifyJwt(secret, jwtToken) if err != nil { return "", err @@ -209,7 +265,116 @@ func (p *Plugin) updateJwtUserInfo(jwtToken string, user *model.User) (string, e return signClaims(secret, claims) } -func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingID string, meetingTopic string, _ bool, rootID string) (string, error) { +func (p *Plugin) getSignClaimsJaaS(jwtToken string, sanitizedUser *model.User) (string, error) { + claims, err := getJwtClaims(jwtToken) + if err != nil { + return "", err + } + + config := p.API.GetConfig() + if config.PrivacySettings.ShowFullName == nil || !*config.PrivacySettings.ShowFullName { + sanitizedUser.FirstName = "" + sanitizedUser.LastName = "" + } + if config.PrivacySettings.ShowEmailAddress == nil || !*config.PrivacySettings.ShowEmailAddress { + sanitizedUser.Email = "" + } + + newContext := JaaSContext{ + User: JaaSUser{ + Avatar: fmt.Sprintf("%s/api/v4/users/%s/image?_=%d", *config.ServiceSettings.SiteURL, sanitizedUser.Id, sanitizedUser.LastPictureUpdate), + Name: sanitizedUser.GetDisplayName(model.ShowNicknameFullName), + Email: sanitizedUser.Email, + ID: sanitizedUser.Id, + Moderator: `true`, + }, + Features: claims.Context.Features, + } + + claims.Context = newContext + + return signClaimsJaaS(p.getConfiguration().JaaSApiKey, p.getConfiguration().JaaSPrivateKey, claims) +} + +func (p *Plugin) setJWTClaims(user *model.User, userType string) (string, error) { + // User did not specify a jwt, generate a new jwt for user + const ExpTimeDelaySec = 7200 + permission := "true" + if userType != typeUser { + permission = "false" + } + + claims := JaaSClaims{} + claims.Issuer = jaasIssuerClaim + claims.Audience = jaasAudienceClaim + claims.Subject = p.getConfiguration().JaaSAppID + claims.Room = "*" + claims.Exp = time.Now().Unix() + ExpTimeDelaySec + claims.Nbf = time.Now().Unix() + claims.Context.Features.LiveStreaming = permission + claims.Context.Features.Recording = permission + claims.Context.Features.OutboundCall = permission + claims.Context.Features.Transcription = permission + claims.Context.User.Avatar = "" + claims.Context.User.Email = "" + if userType == typeUser { + claims.Context.User.Email = user.Email + } + claims.Context.User.ID = user.Id + claims.Context.User.Moderator = permission + claims.Context.User.Name = user.GetFullName() + + jwtToken, err := signClaimsJaaS(p.getConfiguration().JaaSApiKey, p.getConfiguration().JaaSPrivateKey, &claims) + if err != nil { + mlog.Error(fmt.Sprintf("Error generating JaaS token for %s", userType), mlog.Err(err)) + return "", errors.Wrap(err, fmt.Sprintf("failed creating new JaaS token for %s %s", userType, user.Id)) + } + + return jwtToken, nil +} + +func (p *Plugin) generateJaaSJwtForUser(user *model.User) (string, error) { + return p.setJWTClaims(user, typeUser) +} + +func (p *Plugin) generateJaaSJwtForGuest(userid string) (string, error) { + user := &model.User{ + Id: userid, + } + + return p.setJWTClaims(user, typeGuest) +} + +func (p *Plugin) getJaaSSettings(jwtToken string, path string, user *model.User) (*JaaSSettings, error) { + if user != nil { + claims, err := getJwtClaims(jwtToken) + if err != nil { + jwtToken, err = p.generateJaaSJwtForUser(user) + if err != nil { + mlog.Error("Failed to generate new token for user!") + return nil, err + } + } else if claims.Context.User.ID != user.Id { + return nil, errors.New("not authorized") + } + } else { + var err error + jwtToken, err = p.generateJaaSJwtForGuest(uuid.New().String() + "-guest") + if err != nil { + mlog.Error("Error generating JaaS token for guest", mlog.Err(err)) + return nil, errors.New("failed creating new JaaS token for guest") + } + } + + var settings JaaSSettings + settings.Jwt = jwtToken + settings.Room = path + + return &settings, nil +} + +// func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingID string, meetingTopic string, rootID string) (string, error) { +func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingID string, meetingTopic string, rootID string) (string, error) { l := p.b.GetServerLocalizer() if meetingID == "" { meetingID = encodeJitsiMeetingID(meetingTopic) @@ -218,10 +383,16 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI } meetingID += randomString(LETTERS, 20) } + meetingPersonal := false + defaultSelectedMeetingTopic := "Jitsi Meeting" + if p.getConfiguration().UseJaaS { + defaultSelectedMeetingTopic = "JaaS Meeting" + } + defaultMeetingTopic := p.b.LocalizeDefaultMessage(l, &i18n.Message{ ID: "jitsi.start_meeting.default_meeting_topic", - Other: "Jitsi Meeting", + Other: defaultSelectedMeetingTopic, }) if len(meetingTopic) < 1 { @@ -264,44 +435,38 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI meetingID = generateEnglishTitleName() } } - jitsiURL := strings.TrimSpace(p.getConfiguration().GetJitsiURL()) - jitsiURL = strings.TrimRight(jitsiURL, "/") - meetingURL := jitsiURL + "/" + meetingID - meetingLink := meetingURL + + meetingIDLabel := meetingID + + if p.getConfiguration().UseJaaS { + appID := p.getConfiguration().JaaSAppID + meetingID = appID + "/" + meetingID + } + + var meetingURL string + var meetingLink string var meetingLinkValidUntil = time.Time{} - JWTMeeting := p.getConfiguration().JitsiJWT + JWTMeeting := p.getConfiguration().JitsiJWT || p.getConfiguration().UseJaaS var jwtToken string - if JWTMeeting { - // Error check is done in configuration.IsValid() - jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) + meetingUntil := "" - meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) + if p.getConfiguration().UseJaaS { + meetingURL = strings.TrimRight(strings.TrimSpace(*p.API.GetConfig().ServiceSettings.SiteURL+"/plugins/jitsi/public/jaas/jaas.html"), "/") + meetingURL = meetingURL + "?meetingID=" + meetingID + meetingLink = meetingURL - claims := Claims{} - claims.Issuer = p.getConfiguration().JitsiAppID - claims.Audience = []string{p.getConfiguration().JitsiAppID} - claims.ExpiresAt = jwt.NewNumericDate(meetingLinkValidUntil) - claims.Subject = jURL.Hostname() - claims.Room = meetingID + meetingLinkValidUntil = time.Now().Add(time.Duration(DefaultValidityOfMeetingLinkInMinutes) * time.Minute) var err2 error - jwtToken, err2 = signClaims(p.getConfiguration().JitsiAppSecret, &claims) + jwtToken, err2 = p.generateJaaSJwtForUser(user) if err2 != nil { return "", err2 } - meetingURL = meetingURL + "?jwt=" + jwtToken - } - if meetingTopic == "" { - meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+defaultMeetingTopic+"\"") - } else { - meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+meetingTopic+"\"") - } + meetingURL = meetingURL + "&jwt=" + jwtToken - meetingUntil := "" - if JWTMeeting { meetingUntil = p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ DefaultMessage: &i18n.Message{ ID: "jitsi.start_meeting.meeting_link_valid_until", @@ -309,6 +474,47 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI }, TemplateData: map[string]string{"Datetime": meetingLinkValidUntil.Format("Mon Jan 2 15:04:05 -0700 MST 2006")}, }) + } else { + jitsiURL := strings.TrimRight(strings.TrimSpace(p.getConfiguration().GetJitsiURL()), "/") + meetingURL = jitsiURL + "/" + meetingID + meetingLink = meetingURL + if JWTMeeting { + // Error check is done in configuration.IsValid() + jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) + + meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) + + claims := Claims{} + claims.Issuer = p.getConfiguration().JitsiAppID + claims.Audience = []string{p.getConfiguration().JitsiAppID} + claims.ExpiresAt = jwt.NewNumericDate(meetingLinkValidUntil) + claims.Subject = jURL.Hostname() + claims.Room = meetingID + + var err2 error + jwtToken, err2 = signClaims(p.getConfiguration().JitsiAppSecret, &claims) + if err2 != nil { + return "", err2 + } + + meetingURL = meetingURL + "?jwt=" + jwtToken + } + + if JWTMeeting { + meetingUntil = p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: "jitsi.start_meeting.meeting_link_valid_until", + Other: "Meeting link valid until: {{.Datetime}}", + }, + TemplateData: map[string]string{"Datetime": meetingLinkValidUntil.Format("Mon Jan 2 15:04:05 -0700 MST 2006")}, + }) + } + } + + if meetingTopic == "" { + meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+defaultMeetingTopic+"\"") + } else { + meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+meetingTopic+"\"") } meetingTypeString := p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ @@ -374,6 +580,8 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI "meeting_personal": meetingPersonal, "meeting_topic": meetingTopic, "default_meeting_topic": defaultMeetingTopic, + "jaas_meeting": p.getConfiguration().UseJaaS, + "meeting_id_label": meetingIDLabel, }, RootId: rootID, } @@ -516,6 +724,7 @@ func (p *Plugin) getUserConfig(userID string) (*UserConfig, error) { return &UserConfig{ Embedded: p.getConfiguration().JitsiEmbedded, NamingScheme: p.getConfiguration().JitsiNamingScheme, + UseJaas: p.getConfiguration().UseJaaS, ShowPrejoinPage: p.getConfiguration().JitsiPrejoinPage, }, nil } @@ -526,6 +735,7 @@ func (p *Plugin) getUserConfig(userID string) (*UserConfig, error) { return nil, err } + userConfig.UseJaas = p.getConfiguration().UseJaaS return &userConfig, nil } diff --git a/server/plugin_test.go b/server/plugin_test.go index 71dfd266..5b730151 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/cristalhq/jwt/v2" + "github.com/cristalhq/jwt/v3" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/mattermost/mattermost/server/public/pluginapi/i18n" @@ -77,7 +77,7 @@ func TestStartMeeting(t *testing.T) { b, _ := json.Marshal(UserConfig{Embedded: false, NamingScheme: "mattermost"}) apiMock.On("KVGet", "config_test-id", mock.Anything).Return(b, nil) - meetingID, err := p.startMeeting(&testUser, &testChannel, "", "", false, "") + meetingID, err := p.startMeeting(&testUser, &testChannel, "", "", "") require.Nil(t, err) require.Regexp(t, "^test-username-", meetingID) }) @@ -89,7 +89,7 @@ func TestStartMeeting(t *testing.T) { b, _ := json.Marshal(UserConfig{Embedded: false, NamingScheme: "mattermost"}) apiMock.On("KVGet", "config_test-id", mock.Anything).Return(b, nil) - meetingID, err := p.startMeeting(&testUser, &testChannel, "", "Test topic", false, "") + meetingID, err := p.startMeeting(&testUser, &testChannel, "", "Test topic", "") require.Nil(t, err) require.Regexp(t, "^Test-topic-", meetingID) }) @@ -101,7 +101,7 @@ func TestStartMeeting(t *testing.T) { b, _ := json.Marshal(UserConfig{Embedded: false, NamingScheme: "mattermost"}) apiMock.On("KVGet", "config_test-id", mock.Anything).Return(b, nil) - meetingID, err := p.startMeeting(&testUser, &testChannel, "test-id", "", false, "") + meetingID, err := p.startMeeting(&testUser, &testChannel, "test-id", "", "") require.Nil(t, err) require.Regexp(t, "^test-username-", meetingID) }) @@ -116,7 +116,7 @@ func TestStartMeeting(t *testing.T) { b, _ := json.Marshal(UserConfig{Embedded: false, NamingScheme: "mattermost"}) apiMock.On("KVGet", "config_test-id", mock.Anything).Return(b, nil) - meetingID, err := p.startMeeting(&testUser, &testChannel, "test-id", "Test topic", false, "") + meetingID, err := p.startMeeting(&testUser, &testChannel, "test-id", "Test topic", "") require.Nil(t, err) require.Equal(t, "test-id", meetingID) }) diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index c447ac63..205d4f87 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -85,7 +85,7 @@ "no-compare-neg-zero": 2, "no-cond-assign": [2, "except-parens"], "no-confusing-arrow": 2, - "no-console": 2, + "no-console": 0, "no-const-assign": 2, "no-constant-condition": 2, "no-debugger": 2, @@ -147,7 +147,8 @@ "no-self-assign": [2, {"props": true}], "no-self-compare": 2, "no-sequences": 2, - "no-shadow": [2, {"hoist": "functions"}], + "no-shadow": "off", + "@typescript-eslint/no-shadow": ["error"], "no-shadow-restricted-names": 2, "no-spaced-func": 2, "no-tabs": 0, @@ -246,7 +247,7 @@ "react/no-unused-prop-types": [1, {"skipShapeProps": true}], "react/prefer-es6-class": 2, "react/prefer-stateless-function": 0, - "react/prop-types": 2, + "react/prop-types": "off", "react/require-default-props": 0, "react/require-optimization": 1, "react/require-render-return": 2, diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 7b1ca538..d6735639 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1,14 +1,49 @@ { + "jaas.api-key-id": "Api key ID for JaaS Authentication:", + "jaas.api-key-id-description": "Specify your JaaS Api key ID. You can get the Api key ID from https://jaas.8x8.vc/#/apikeys.", + "jaas.app-id": "AppID for JaaS Authentication:", + "jaas.app-id-description": "Specify your JaaS AppID. You can get the AppID from https://jaas.8x8.vc/#/apikeys.", + "jaas.embed-meeting": "Embed JaaS video inside Mattermost:", + "jaas.embed-meeting-description": "(Experimental) When true, JaaS video is embedded as a floating window inside Mattermost by default.", + "jaas.rsa-key": "RSA Private key for JaaS Authentication:", + "jaas.rsa-key-description": "Specify your JaaS private key. You can get the private key by generating an API key pair from https://jaas.8x8.vc/#/apikeys OR from https://jaas.8x8.vc/#/start-guide in the \"Api Key\" section. (NOTE: While generating the key manually make sure to set the size to be 2048 and not 4096).", + "jaas.default-title": "JaaS Meeting", + "jitsi.allow-user": "Allow user to select meeting name", + "jitsi.app-id": "App ID for JWT Authentication:", + "jitsi.app-id-description": "(Optional) The app ID used for authentication by the Jitsi server and JWT token generator.", + "jitsi.app-secret": "App Secret for JWT Authentication:", + "jitsi.app-secret-description": "(Optional) The app secret used for authentication by the Jitsi server and JWT token generator.", "jitsi.close": "Close", + "jitsi.context-specific": "Mattermost context specific names. Combination of team name, channel name, and random text in Public and Private channels; personal meeting name in Direct and Group Message channels.", "jitsi.creator-has-started-a-meeting": "{creator} has started a meeting", "jitsi.default-title": "Jitsi Meeting", + "jitsi.embed-video": "Embed Jitsi video inside Mattermost:", + "jitsi.embed-video-description": "(Experimental) When true, Jitsi video is embedded as a floating window inside Mattermost by default. Users can override this setting with \"/jitsi settings\".", + "jitsi.enable-compatibility-mode": "Enable Compatibility Mode:", + "jitsi.enable-compatibility-mode-description": "(Insecure) If your Jitsi server is not compatible with this plugin, include the JavaScript API hosted on your Jitsi server directly in Mattermost instead of the default API version provided by the plugin. WARNING: Enabling this setting can compromise the security of your Mattermost system, if your Jitsi server is not fully trusted and allows direct modification of program files. Use with caution.", + "jitsi.input-enable-jass": "JaaS", + "jitsi.input-enable-jitsi": "Jitsi", "jitsi.join-meeting": "JOIN MEETING", + "jitsi.link-expiry-time": "Meeting Link Expiry Time (minutes):", + "jitsi.link-expiry-time-description": "(Optional) The number of minutes from when the meeting link is created to when it becomes invalid. Minimum is 1 minute. Only applies if using JWT authentication for your Jitsi server.", "jitsi.link-valid-until": "Meeting link valid until: ", "jitsi.maximize": "Maximize", "jitsi.meeting-id": "Meeting ID: ", + "jitsi.meeting-names": "Jitsi Meeting Names:", + "jitsi.meeting-names-description": "Select how meeting names are generated by default. Users can override this setting with \"/jitsi settings\".", "jitsi.minimize": "Minimize", "jitsi.move-down": "Move down", "jitsi.move-up": "Move up", "jitsi.open-in-new-tab": "Open in new tab", - "jitsi.personal-meeting-id": "Personal Meeting ID (PMI): " + "jitsi.personal-meeting-id": "Personal Meeting ID (PMI): ", + "jitsi.random-english-words": "Random English words in title case (e.g. PlayfulDragonsObserveCuriously)", + "jitsi.server-description": "Select the type of jitsi server you want to use.", + "jitsi.server": "Server:", + "jitsi.server-url": "Jitsi Server URL:", + "jitsi.server-url-description": "The url for your Jitsi server, for example https://jitsi.example.com. Defaults to https://meet.jit.si, which is the public server provided by Jitsi.", + "jitsi.use-jwt": "Use JWT Authentication for Jitsi:", + "jitsi.use-jwt-description": "(Optional) If your Jitsi server uses JSON Web Tokens (JWT) for authentication, set this value to true.", + "jitsi.uuid": "UUID (universally unique identifier)", + "jitsi.true": "true", + "jitsi.false": "false" } diff --git a/webapp/package-lock.json b/webapp/package-lock.json index ca9cfec9..c1b2d1a2 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -10,10 +10,13 @@ "dependencies": { "core-js": "3.6.5", "mattermost-redux": "5.23.0", + "prop-types": "15.7.2", "react": "16.14.0", + "react-dom": "16.14.0", "react-intl": "4.6.3", "react-redux": "7.2.0", - "redux": "4.0.5" + "redux": "4.0.5", + "redux-thunk": "2.3.0" }, "devDependencies": { "@babel/cli": "7.16.8", @@ -49,6 +52,7 @@ "eslint-plugin-import": "2.25.4", "eslint-plugin-react": "7.28.0", "file-loader": "6.0.0", + "html-webpack-plugin": "4.5.2", "identity-obj-proxy": "3.0.0", "jest": "26.0.1", "jest-canvas-mock": "2.2.0", @@ -3720,6 +3724,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "dev": true + }, "node_modules/@types/invariant": { "version": "2.2.37", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.37.tgz", @@ -3867,11 +3877,66 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" }, + "node_modules/@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/tapable": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", + "dev": true + }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/@types/yargs": { "version": "15.0.19", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", @@ -4710,6 +4775,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.reduce": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", + "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", @@ -5698,6 +5782,22 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camel-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -5966,6 +6066,18 @@ "node": ">= 0.4" } }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6764,6 +6876,17 @@ "node": ">=18" } }, + "node_modules/deprecated-react-native-prop-types/node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -6857,6 +6980,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6944,6 +7076,22 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -7179,6 +7327,17 @@ "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0" } }, + "node_modules/enzyme-adapter-utils/node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/enzyme-adapter-utils/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -9203,6 +9362,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/hermes-estree": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.15.0.tgz", @@ -9325,6 +9493,117 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-minifier-terser/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/html-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "engines": { + "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/html-webpack-plugin/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -11717,6 +11996,21 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -13103,6 +13397,22 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/no-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/nocache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", @@ -13522,6 +13832,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", + "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", + "dev": true, + "dependencies": { + "array.prototype.reduce": "^1.0.6", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "safe-array-concat": "^1.0.0" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.hasown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", @@ -13903,6 +14232,22 @@ "readable-stream": "^2.1.5" } }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -14003,6 +14348,22 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -14153,6 +14514,16 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, "node_modules/pretty-format": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", @@ -14248,13 +14619,13 @@ } }, "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "react-is": "^16.8.1" } }, "node_modules/prop-types-exact": { @@ -14530,8 +14901,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -15412,6 +15781,15 @@ "jsesc": "bin/jsesc" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/remote-redux-devtools": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", @@ -15440,6 +15818,127 @@ "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", "dev": true }, + "node_modules/renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + } + }, + "node_modules/renderkid/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -16095,7 +16594,6 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18058,12 +18556,28 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "node_modules/util/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -22351,6 +22865,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "dev": true + }, "@types/invariant": { "version": "2.2.37", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.37.tgz", @@ -22502,11 +23022,65 @@ "@types/react": "*" } }, + "@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "@types/tapable": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, "@types/yargs": { "version": "15.0.19", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", @@ -23138,6 +23712,19 @@ "es-shim-unscopables": "^1.0.0" } }, + "array.prototype.reduce": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", + "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, "arraybuffer.prototype.slice": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", @@ -23948,6 +24535,24 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -24146,6 +24751,15 @@ } } }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -24799,6 +25413,19 @@ "@react-native/normalize-colors": "^0.73.0", "invariant": "^2.2.4", "prop-types": "^15.8.1" + }, + "dependencies": { + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + } } }, "des.js": { @@ -24877,6 +25504,15 @@ "esutils": "^2.0.2" } }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, "dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -24937,6 +25573,24 @@ "domhandler": "^5.0.3" } }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -25125,6 +25779,17 @@ "semver": "^5.7.1" }, "dependencies": { + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -26715,6 +27380,12 @@ "function-bind": "^1.1.2" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hermes-estree": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.15.0.tgz", @@ -26821,6 +27492,93 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + } + } + }, + "html-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + } + } + }, "htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -28707,6 +29465,23 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -29828,6 +30603,24 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "nocache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", @@ -30169,6 +30962,19 @@ "es-abstract": "^1.22.1" } }, + "object.getownpropertydescriptors": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", + "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", + "dev": true, + "requires": { + "array.prototype.reduce": "^1.0.6", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "safe-array-concat": "^1.0.0" + } + }, "object.hasown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", @@ -30452,6 +31258,24 @@ "readable-stream": "^2.1.5" } }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -30533,6 +31357,24 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -30644,6 +31486,16 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, "pretty-format": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", @@ -30726,13 +31578,13 @@ } }, "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "react-is": "^16.8.1" } }, "prop-types-exact": { @@ -30966,8 +31818,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "dev": true, - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -31693,6 +32543,12 @@ } } }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true + }, "remote-redux-devtools": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", @@ -31720,6 +32576,98 @@ "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", "dev": true }, + "renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "dev": true, + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -32224,7 +33172,6 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -33795,6 +34742,22 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index 8ad4eac1..99cd47b3 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -51,6 +51,7 @@ "eslint-plugin-import": "2.25.4", "eslint-plugin-react": "7.28.0", "file-loader": "6.0.0", + "html-webpack-plugin": "4.5.2", "identity-obj-proxy": "3.0.0", "jest": "26.0.1", "jest-canvas-mock": "2.2.0", @@ -65,10 +66,13 @@ "dependencies": { "core-js": "3.6.5", "mattermost-redux": "5.23.0", + "prop-types": "15.7.2", "react": "16.14.0", + "react-dom": "16.14.0", "react-intl": "4.6.3", "react-redux": "7.2.0", - "redux": "4.0.5" + "redux": "4.0.5", + "redux-thunk": "2.3.0" }, "jest": { "snapshotSerializers": [ diff --git a/webapp/src/actions/index.ts b/webapp/src/actions/index.ts index 3a02f0e6..31a5e067 100644 --- a/webapp/src/actions/index.ts +++ b/webapp/src/actions/index.ts @@ -108,3 +108,26 @@ export function setUserStatus(userId: string, status: string): ActionFunc { } }; } + +export function sendEphemeralPost(message: string, channelID: string, userID: string): ActionFunc { + return (dispatch: DispatchFunc): ActionResult => { + const timestamp = Date.now(); + const post = { + id: 'jitsi' + timestamp, + user_id: userID, + channel_id: channelID, + message, + type: 'system_ephemeral', + create_at: timestamp, + update_at: timestamp + }; + + dispatch({ + type: PostTypes.RECEIVED_NEW_POST, + data: post, + channelID + }); + + return {data: post}; + }; +} diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx new file mode 100644 index 00000000..c6511b75 --- /dev/null +++ b/webapp/src/app.tsx @@ -0,0 +1,16 @@ +import React, {useEffect} from 'react'; +import {useDispatch} from 'react-redux'; +import {loadConfig} from './actions'; + +const App = (): React.ReactNode => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(loadConfig()); + }, []); + + // This container is used just for making the API call for fetching the config, it doesn't render anything. + return null; +}; + +export default App; diff --git a/webapp/src/components/InputField/index.tsx b/webapp/src/components/InputField/index.tsx new file mode 100644 index 00000000..e34b3c43 --- /dev/null +++ b/webapp/src/components/InputField/index.tsx @@ -0,0 +1,55 @@ +import React, {ReactNode} from 'react'; + +import {InputElementType, InputTypes} from 'types'; + +type Props = { + heading: string | ReactNode; + type: InputTypes; + placeholder?: string; + min?: number; + rows?: number; + defaultValue?: number | string; + onChange: (e: React.ChangeEvent) => void; + value: string | number; + disabled?: boolean; + description: string | ReactNode; +} + +export const TextInput = ({heading, type, placeholder, min, rows, defaultValue, onChange, value, disabled, description}: Props) => { + return ( + <> +
+ +
+ {type === InputTypes.TextArea ? ( +