Merge branch 'main' of github.com:openziti/zrok into stefanadelbert-linux-share-private

This commit is contained in:
Kenneth Bingham 2024-06-27 11:39:58 -04:00
commit 77e1a667f2
No known key found for this signature in database
GPG Key ID: 31709281860130B6
51 changed files with 1013 additions and 595 deletions

View File

@ -1,5 +1,41 @@
# CHANGELOG
## v0.4.34
FIX: Fix for mixing limited and unlimited (-1) resource counts in the limits system (https://github.com/openziti/zrok/issues/680)
## v0.4.33
FIX: Fix for log message in `Agent.CanAccessShare` (`"account '#%d' over frontends per share limit '%d'"`), which was not returning the correct limit value.
FIX: Properly set `permission_mode` in `frontends` when createing a private frontend using `zrok access private` (https://github.com/openziti/zrok/issues/677)
CHANGE: Updated `react-bootstrap` to version `2.10.2` (web console).
CHANGE: Updated `@mui/material` to version `5.15.18` (web console).
CHANGE: Updated `react` and `react-dom` to version `18.3.1` (web console).
CHANGE: Updated `recharts` to version `2.12.7` (web console).
CHANGE: Updated `react-router-dom` to version `6.23.1` (web console).
CHANGE: Updated `axios` to version `1.7.2` for (node SDK).
CHANGE: Updated `@openziti/ziti-sdk-nodejs` to version `0.17.0` (node SDK).
## v0.4.32
FEATURE: New permission mode support for public frontends. Open permission mode frontends are available to all users in the service instance. Closed permission mode frontends reference the new `frontend_grants` table that can be used to control which accounts are allowed to create shares using that frontend. `zrok admin create frontend` now supports `--closed` flag to create closed permission mode frontends (https://github.com/openziti/zrok/issues/539)
FEATURE: New config `defaultFrontend` that specifies the default frontend to be used for an environment. Provides the default `--frontend` for `zrok share public` and `zrok reserve public` (https://github.com/openziti/zrok/issues/663)
FEATURE: Resource count limits now include `share_frontends` to limit the number of frontends that are allowed to make connections to a share (https://github.com/openziti/zrok/issues/650)
CHANGE: The frontend selection flag used by `zrok share public` and `zrok reserve public` has been changed from `--frontends` to `--frontend`
FIX: use controller config spec v4 in the Docker instance
## v0.4.31
FEATURE: New "limits classes" limits implementation (https://github.com/openziti/zrok/issues/606). This new feature allows for extensive limits customization on a per-user basis, with fallback to the global defaults in the controller configuration.

View File

@ -4,6 +4,7 @@ import (
"github.com/openziti/zrok/environment"
"github.com/openziti/zrok/rest_client_zrok/admin"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/sdk/golang/sdk"
"github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -15,7 +16,8 @@ func init() {
}
type adminCreateFrontendCommand struct {
cmd *cobra.Command
cmd *cobra.Command
closed bool
}
func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
@ -25,6 +27,7 @@ func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
Args: cobra.ExactArgs(3),
}
command := &adminCreateFrontendCommand{cmd: cmd}
cmd.Flags().BoolVar(&command.closed, "closed", false, "Enabled closed permission mode")
cmd.Run = command.run
return command
}
@ -44,11 +47,16 @@ func (cmd *adminCreateFrontendCommand) run(_ *cobra.Command, args []string) {
panic(err)
}
permissionMode := sdk.OpenPermissionMode
if cmd.closed {
permissionMode = sdk.ClosedPermissionMode
}
req := admin.NewCreateFrontendParams()
req.Body = &rest_model_zrok.CreateFrontendRequest{
ZID: zId,
PublicName: publicName,
URLTemplate: urlTemplate,
ZID: zId,
PublicName: publicName,
URLTemplate: urlTemplate,
PermissionMode: string(permissionMode),
}
resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth())

View File

@ -40,6 +40,12 @@ func (cmd *configGetCommand) run(_ *cobra.Command, args []string) {
} else {
fmt.Println("apiEndpoint = <unset>")
}
case "defaultFrontend":
if env.Config() != nil && env.Config().DefaultFrontend != "" {
fmt.Printf("defaultFrontend = %v\n", env.Config().DefaultFrontend)
} else {
fmt.Println("defaultFrontend = <unset>")
}
default:
fmt.Printf("unknown config name '%v'\n", configName)
}

View File

@ -63,6 +63,20 @@ func (cmd *configSetCommand) run(_ *cobra.Command, args []string) {
fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable"))
}
case "defaultFrontend":
if env.Config() == nil {
if err := env.SetConfig(&env_core.Config{DefaultFrontend: value}); err != nil {
tui.Error("unable to save config", err)
}
} else {
cfg := env.Config()
cfg.DefaultFrontend = value
if err := env.SetConfig(cfg); err != nil {
tui.Error("unable to save config", err)
}
}
fmt.Println("zrok configuration updated")
default:
fmt.Printf("unknown config name '%v'\n", configName)
os.Exit(1)

View File

@ -3,7 +3,6 @@ package main
import (
"fmt"
"github.com/openziti/zrok/environment"
"github.com/openziti/zrok/environment/env_core"
"github.com/openziti/zrok/tui"
"github.com/spf13/cobra"
"os"
@ -36,18 +35,25 @@ func (cmd *configUnsetCommand) run(_ *cobra.Command, args []string) {
panic(err)
}
switch configName {
case "apiEndpoint":
if err := env.SetConfig(&env_core.Config{}); err != nil {
if env.Config() != nil {
cfg := env.Config()
switch configName {
case "apiEndpoint":
cfg.ApiEndpoint = ""
if env.IsEnabled() {
fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable"))
}
case "defaultFrontend":
cfg.DefaultFrontend = ""
default:
fmt.Printf("unknown config name '%v'\n", configName)
os.Exit(1)
}
if err := env.SetConfig(cfg); err != nil {
tui.Error("unable to save config", err)
}
fmt.Println("zrok configuration updated")
if env.IsEnabled() {
fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable"))
}
default:
fmt.Printf("unknown config name '%v'\n", configName)
os.Exit(1)
}
}

View File

@ -40,8 +40,13 @@ func newReserveCommand() *reserveCommand {
Args: cobra.RangeArgs(1, 2),
}
command := &reserveCommand{cmd: cmd}
defaultFrontends := []string{"public"}
if root, err := environment.LoadRoot(); err == nil {
defaultFrontend, _ := root.DefaultFrontend()
defaultFrontends = []string{defaultFrontend}
}
cmd.Flags().StringVarP(&command.uniqueName, "unique-name", "n", "", "A unique name for the reserved share (defaults to generated identifier)")
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share")
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontend", defaultFrontends, "Selected frontends to use for the share")
cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode (public|private: proxy, web, caddy, drive) (private: tcpTunnel, udpTunnel, socks, vpn)")
cmd.Flags().BoolVarP(&command.jsonOutput, "json-output", "j", false, "Emit JSON describing the created reserved share")
cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...)")

View File

@ -45,7 +45,12 @@ func newSharePublicCommand() *sharePublicCommand {
Args: cobra.ExactArgs(1),
}
command := &sharePublicCommand{cmd: cmd}
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share")
defaultFrontends := []string{"public"}
if root, err := environment.LoadRoot(); err == nil {
defaultFrontend, _ := root.DefaultFrontend()
defaultFrontends = []string{defaultFrontend}
}
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontend", defaultFrontends, "Selected frontends to use for the share")
cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, caddy, drive}")
cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless")
cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for <target>")

View File

@ -48,8 +48,10 @@ func (cmd *statusCommand) run(_ *cobra.Command, _ []string) {
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleColoredDark)
t.AppendHeader(table.Row{"Config", "Value", "Source"})
apiEndpoint, from := env.ApiEndpoint()
t.AppendRow(table.Row{"apiEndpoint", apiEndpoint, from})
apiEndpoint, apiEndpointFrom := env.ApiEndpoint()
t.AppendRow(table.Row{"apiEndpoint", apiEndpoint, apiEndpointFrom})
defaultFrontend, defaultFrontendFrom := env.DefaultFrontend()
t.AppendRow(table.Row{"defaultFrontend", defaultFrontend, defaultFrontendFrom})
t.Render()
_, _ = fmt.Fprintf(os.Stderr, "\n")

View File

@ -81,7 +81,7 @@ func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_
return share.NewAccessInternalServerError()
}
if _, err := str.CreateFrontend(envId, &store.Frontend{PrivateShareId: &shr.Id, Token: feToken, ZId: envZId}, trx); err != nil {
if _, err := str.CreateFrontend(envId, &store.Frontend{PrivateShareId: &shr.Id, Token: feToken, ZId: envZId, PermissionMode: store.ClosedPermissionMode}, trx); err != nil {
logrus.Errorf("error creating frontend record for user '%v': %v", principal.Email, err)
return share.NewAccessInternalServerError()
}

View File

@ -2,7 +2,6 @@ package controller
import (
"errors"
"github.com/go-openapi/runtime/middleware"
"github.com/lib/pq"
"github.com/mattn/go-sqlite3"
@ -57,11 +56,12 @@ func (h *createFrontendHandler) Handle(params admin.CreateFrontendParams, princi
}
fe := &store.Frontend{
Token: feToken,
ZId: params.Body.ZID,
PublicName: &params.Body.PublicName,
UrlTemplate: &params.Body.URLTemplate,
Reserved: true,
Token: feToken,
ZId: params.Body.ZID,
PublicName: &params.Body.PublicName,
UrlTemplate: &params.Body.URLTemplate,
Reserved: true,
PermissionMode: store.PermissionMode(params.Body.PermissionMode),
}
if _, err := str.CreateGlobalFrontend(fe, tx); err != nil {
perr := &pq.Error{}

View File

@ -134,15 +134,15 @@ func (a *Agent) CanCreateShare(acctId, envId int, reserved, uniqueName bool, _ s
uniqueNames++
}
}
if total+1 > rc.GetShares() {
if rc.GetShares() > store.Unlimited && total+1 > rc.GetShares() {
logrus.Debugf("account '#%d', environment '%d' over shares limit '%d'", acctId, envId, a.cfg.Shares)
return false, nil
}
if reserved && reserveds+1 > rc.GetReservedShares() {
if reserved && rc.GetReservedShares() > store.Unlimited && reserveds+1 > rc.GetReservedShares() {
logrus.Debugf("account '#%d', environment '%d' over reserved shares limit '%d'", acctId, envId, a.cfg.ReservedShares)
return false, nil
}
if reserved && uniqueName && uniqueNames+1 > rc.GetUniqueNames() {
if reserved && uniqueName && rc.GetUniqueNames() > store.Unlimited && uniqueNames+1 > rc.GetUniqueNames() {
logrus.Debugf("account '#%d', environment '%d' over unique names limit '%d'", acctId, envId, a.cfg.UniqueNames)
return false, nil
}
@ -163,30 +163,47 @@ func (a *Agent) CanAccessShare(shrId int, trx *sqlx.Tx) (bool, error) {
return false, err
}
if env.AccountId != nil {
if err := a.str.LimitCheckLock(*env.AccountId, trx); err != nil {
return false, err
}
ul, err := a.getUserLimits(*env.AccountId, trx)
if err != nil {
return false, err
}
if ul.resource.IsGlobal() {
if empty, err := a.str.IsBandwidthLimitJournalEmptyForGlobal(*env.AccountId, trx); err == nil && !empty {
lj, err := a.str.FindLatestBandwidthLimitJournalForGlobal(*env.AccountId, trx)
if scopedBwc, found := ul.scopes[sdk.BackendMode(shr.BackendMode)]; found {
latestScopedJe, err := a.isBandwidthClassLimitedForAccount(*env.AccountId, scopedBwc, trx)
if err != nil {
return false, err
}
if latestScopedJe != nil {
return false, nil
}
} else {
for _, bwc := range ul.bandwidth {
latestJe, err := a.isBandwidthClassLimitedForAccount(*env.AccountId, bwc, trx)
if err != nil {
return false, err
}
if lj.Action == store.LimitLimitAction {
if latestJe != nil {
return false, nil
}
}
} else {
if empty, err := a.str.IsBandwidthLimitJournalEmptyForLimitClass(*env.AccountId, ul.resource.GetLimitClassId(), trx); err == nil && !empty {
lj, err := a.str.FindLatestBandwidthLimitJournalForLimitClass(*env.AccountId, ul.resource.GetLimitClassId(), trx)
if err != nil {
return false, err
}
if lj.Action == store.LimitLimitAction {
return false, nil
}
}
rc := ul.resource
if scopeRc, found := ul.scopes[sdk.BackendMode(shr.BackendMode)]; found {
rc = scopeRc
}
if rc.GetShareFrontends() > store.Unlimited {
fes, err := a.str.FindFrontendsForPrivateShare(shr.Id, trx)
if err != nil {
return false, err
}
if len(fes)+1 > rc.GetShareFrontends() {
logrus.Infof("account '#%d' over frontends per share limit '%d'", *env.AccountId, rc.GetShareFrontends())
return false, nil
}
}
} else {

View File

@ -10,6 +10,7 @@ type Config struct {
Shares int
ReservedShares int
UniqueNames int
ShareFrontends int
Bandwidth *BandwidthPerPeriod
Cycle time.Duration
Enforcing bool
@ -49,6 +50,7 @@ func DefaultConfig() *Config {
Shares: store.Unlimited,
ReservedShares: store.Unlimited,
UniqueNames: store.Unlimited,
ShareFrontends: store.Unlimited,
Bandwidth: DefaultBandwidthPerPeriod(),
Enforcing: false,
Cycle: 15 * time.Minute,

View File

@ -37,6 +37,10 @@ func (rcc *configResourceCountClass) GetUniqueNames() int {
return rcc.cfg.UniqueNames
}
func (rcc *configResourceCountClass) String() string {
return fmt.Sprintf("Config<environments: %d, shares: %d, reservedShares: %d, uniqueNames: %d>", rcc.cfg.Environments, rcc.cfg.Shares, rcc.cfg.ReservedShares, rcc.cfg.UniqueNames)
func (rcc *configResourceCountClass) GetShareFrontends() int {
return rcc.cfg.ShareFrontends
}
func (rcc *configResourceCountClass) String() string {
return fmt.Sprintf("Config<environments: %d, shares: %d, reservedShares: %d, uniqueNames: %d, share_frontends: %d>", rcc.cfg.Environments, rcc.cfg.Shares, rcc.cfg.ReservedShares, rcc.cfg.UniqueNames, rcc.cfg.ShareFrontends)
}

View File

@ -42,7 +42,6 @@ func (ul *userLimits) ignoreBackends(bwc store.BandwidthClass) map[sdk.BackendMo
}
return ignoreBackends
}
return nil
}
func (a *Agent) getUserLimits(acctId int, trx *sqlx.Tx) (*userLimits, error) {
@ -85,7 +84,7 @@ func (a *Agent) isResourceCountClass(alc *store.LimitClass) bool {
if alc.BackendMode != nil {
return false
}
if alc.Environments == store.Unlimited && alc.Shares == store.Unlimited && alc.ReservedShares == store.Unlimited && alc.UniqueNames == store.Unlimited {
if alc.Environments == store.Unlimited && alc.Shares == store.Unlimited && alc.ReservedShares == store.Unlimited && alc.UniqueNames == store.Unlimited && alc.ShareFrontends == store.Unlimited {
return false
}
return true
@ -95,7 +94,7 @@ func (a *Agent) isUnscopedBandwidthClass(alc *store.LimitClass) bool {
if alc.BackendMode != nil {
return false
}
if alc.Environments > store.Unlimited || alc.Shares > store.Unlimited || alc.ReservedShares > store.Unlimited || alc.UniqueNames > store.Unlimited {
if alc.Environments > store.Unlimited || alc.Shares > store.Unlimited || alc.ReservedShares > store.Unlimited || alc.UniqueNames > store.Unlimited || alc.ShareFrontends > store.Unlimited {
return false
}
if alc.PeriodMinutes < 1 {

View File

@ -116,6 +116,17 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
logrus.Error(err)
return share.NewShareNotFound()
}
if sfe.PermissionMode == store.ClosedPermissionMode {
granted, err := str.IsFrontendGrantedToAccount(int(principal.ID), sfe.Id, trx)
if err != nil {
logrus.Error(err)
return share.NewShareInternalServerError()
}
if !granted {
logrus.Errorf("'%v' is not granted access to frontend '%v'", principal.Email, frontendSelection)
return share.NewShareNotFound()
}
}
if sfe != nil && sfe.UrlTemplate != nil {
frontendZIds = append(frontendZIds, sfe.ZId)
frontendTemplates = append(frontendTemplates, *sfe.UrlTemplate)

View File

@ -14,28 +14,28 @@ type Frontend struct {
PublicName *string
UrlTemplate *string
Reserved bool
Deleted bool
PermissionMode PermissionMode
}
func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5, $6, $7) returning id")
stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved, permission_mode) values ($1, $2, $3, $4, $5, $6, $7, $8) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing frontends insert statement")
}
var id int
if err := stmt.QueryRow(envId, f.PrivateShareId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil {
if err := stmt.QueryRow(envId, f.PrivateShareId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved, f.PermissionMode).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing frontends insert statement")
}
return id, nil
}
func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into frontends (token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5) returning id")
stmt, err := tx.Prepare("insert into frontends (token, z_id, public_name, url_template, reserved, permission_mode) values ($1, $2, $3, $4, $5, $6) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing global frontends insert statement")
}
var id int
if err := stmt.QueryRow(f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil {
if err := stmt.QueryRow(f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved, f.PermissionMode).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing global frontends insert statement")
}
return id, nil
@ -122,12 +122,12 @@ func (str *Store) FindFrontendsForPrivateShare(shrId int, tx *sqlx.Tx) ([]*Front
}
func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, updated_at = current_timestamp where id = $8"
sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, permission_mode = $8, updated_at = current_timestamp where id = $9"
stmt, err := tx.Prepare(sql)
if err != nil {
return errors.Wrap(err, "error preparing frontends update statement")
}
_, err = stmt.Exec(fe.EnvironmentId, fe.PrivateShareId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.Id)
_, err = stmt.Exec(fe.EnvironmentId, fe.PrivateShareId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.PermissionMode, fe.Id)
if err != nil {
return errors.Wrap(err, "error executing frontends update statement")
}

View File

@ -0,0 +1,18 @@
package store
import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
func (str *Store) IsFrontendGrantedToAccount(acctId, frontendId int, trx *sqlx.Tx) (bool, error) {
stmt, err := trx.Prepare("select count(0) from frontend_grants where account_id = $1 AND frontend_id = $2")
if err != nil {
return false, errors.Wrap(err, "error preparing frontend_grants select statement")
}
var count int
if err := stmt.QueryRow(acctId, frontendId).Scan(&count); err != nil {
return false, errors.Wrap(err, "error querying frontend_grants count")
}
return count > 0, nil
}

View File

@ -22,6 +22,7 @@ type ResourceCountClass interface {
GetShares() int
GetReservedShares() int
GetUniqueNames() int
GetShareFrontends() int
}
type BandwidthClass interface {
@ -37,11 +38,13 @@ type BandwidthClass interface {
type LimitClass struct {
Model
Label *string
BackendMode *sdk.BackendMode
Environments int
Shares int
ReservedShares int
UniqueNames int
ShareFrontends int
PeriodMinutes int
RxBytes int64
TxBytes int64
@ -77,6 +80,10 @@ func (lc LimitClass) GetUniqueNames() int {
return lc.UniqueNames
}
func (lc LimitClass) GetShareFrontends() int {
return lc.ShareFrontends
}
func (lc LimitClass) GetBackendMode() sdk.BackendMode {
if lc.BackendMode == nil {
return ""
@ -105,7 +112,12 @@ func (lc LimitClass) GetLimitAction() LimitAction {
}
func (lc LimitClass) String() string {
out := fmt.Sprintf("LimitClass<#%d", lc.Id)
out := "LimitClass<"
if lc.Label != nil && *lc.Label != "" {
out += "'" + *lc.Label + "'"
} else {
out += fmt.Sprintf("#%d", lc.Id)
}
if lc.BackendMode != nil {
out += fmt.Sprintf(", backendMode: '%s'", *lc.BackendMode)
}
@ -121,6 +133,9 @@ func (lc LimitClass) String() string {
if lc.UniqueNames > Unlimited {
out += fmt.Sprintf(", uniqueNames: %d", lc.UniqueNames)
}
if lc.ShareFrontends > Unlimited {
out += fmt.Sprintf(", shareFrontends: %d", lc.ShareFrontends)
}
if lc.RxBytes > Unlimited || lc.TxBytes > Unlimited || lc.TotalBytes > Unlimited {
out += fmt.Sprintf(", periodMinutes: %d", lc.PeriodMinutes)
}
@ -140,12 +155,12 @@ func (lc LimitClass) String() string {
var _ BandwidthClass = (*LimitClass)(nil)
func (str *Store) CreateLimitClass(lc *LimitClass, trx *sqlx.Tx) (int, error) {
stmt, err := trx.Prepare("insert into limit_classes (backend_mode, environments, shares, reserved_shares, unique_names, period_minutes, rx_bytes, tx_bytes, total_bytes, limit_action) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id")
stmt, err := trx.Prepare("insert into limit_classes (label, backend_mode, environments, shares, reserved_shares, unique_names, share_frontends, period_minutes, rx_bytes, tx_bytes, total_bytes, limit_action) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing limit_classes insert statement")
}
var id int
if err := stmt.QueryRow(lc.BackendMode, lc.Environments, lc.Shares, lc.ReservedShares, lc.UniqueNames, lc.PeriodMinutes, lc.RxBytes, lc.TxBytes, lc.TotalBytes, lc.LimitAction).Scan(&id); err != nil {
if err := stmt.QueryRow(lc.Label, lc.BackendMode, lc.Environments, lc.Shares, lc.ReservedShares, lc.UniqueNames, lc.ShareFrontends, lc.PeriodMinutes, lc.RxBytes, lc.TxBytes, lc.TotalBytes, lc.LimitAction).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing limit_classes insert statement")
}
return id, nil

View File

@ -7,14 +7,6 @@ const (
WarningLimitAction LimitAction = "warning"
)
type LimitScope string
const (
AccountLimitScope LimitScope = "account"
EnvironmentLimitScope LimitScope = "environment"
ShareLimitScope LimitScope = "share"
)
type PermissionMode string
const (

View File

@ -18,7 +18,6 @@ type Share struct {
Reserved bool
UniqueName bool
PermissionMode PermissionMode
Deleted bool
}
func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) {

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column share_frontends int not null default (-1);

View File

@ -0,0 +1,17 @@
-- +migrate Up
alter table frontends add column permission_mode permission_mode_type not null default('open');
create table frontend_grants (
id serial primary key,
account_id integer references accounts (id) not null,
frontend_id integer references frontends (id) not null,
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp),
deleted boolean not null default(false)
);
create index frontend_grants_account_id_idx on frontend_grants (account_id);
create index frontend_grants_frontend_id_idx on frontend_grants (frontend_id);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column label varchar(32);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column share_frontends int not null default (-1);

View File

@ -0,0 +1,17 @@
-- +migrate Up
alter table frontends add column permission_mode string not null default('open');
create table frontend_grants (
id integer primary key,
account_id integer references accounts (id) not null,
frontend_id integer references frontends (id) not null,
created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')),
updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')),
deleted boolean not null default(false)
);
create index frontend_grants_account_id_idx on frontend_grants (account_id);
create index frontend_grants_frontend_id_idx on frontend_grants (frontend_id);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column label varchar(32);

View File

@ -90,9 +90,9 @@ ZROK_OAUTH_GOOGLE_CLIENT_ID=abcd1234
ZROK_OAUTH_GOOGLE_CLIENT_SECRET=abcd1234
# zrok version, e.g., 1.0.0
ZROK_IMAGE_TAG=latest
ZROK_CLI_TAG=latest
# ziti version, e.g., 1.0.0
ZITI_IMAGE_TAG=latest
ZITI_CLI_TAG=latest
```
### Start the Docker Compose Project

View File

@ -4,7 +4,7 @@
# /___|_| \___/|_|\_\
# controller configuration
v: 3
v: 4
admin:
# generate these admin tokens from a source of randomness, e.g.
# LC_ALL=C tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c32

View File

@ -20,7 +20,7 @@ The limits agent is responsible for controlling the number of resources in use (
### Types of Limits
Limits can be specified that control the number of environments, shares, reserved shares, and unique names that can be created by an account. Limits that control the allowed number of resources are called _resource count limits_.
Limits can be specified that control the number of environments, shares, reserved shares, unique names, and frontends per-share that can be created by an account. Limits that control the allowed number of resources are called _resource count limits_.
Limits can be specified to control the amount of data that can be transferred within a time period. Limits that control the amount of data that can be transferred are called _bandwidth limits_.
@ -40,6 +40,7 @@ limits:
shares: -1
reserved_shares: -1
unique_names: -1
share_frontends: -1
bandwidth:
period: 5m
warning:
@ -64,7 +65,7 @@ The `cycle` value controls how frequently the limits agent will evaluate enforce
### Global Resouce Count Limits
The `environments`, `shares`, `reserved_shares`, and `unique_names` specify the resource count limits, globally for the service instance.
The `environments`, `shares`, `reserved_shares`, `unique_names`, and `share_frontends` specify the resource count limits, globally for the service instance.
These resource counts will be applied to all users in the service instance by default.
@ -91,11 +92,13 @@ Limit classes are created by creating a record in the `limit_classes` table in t
```sql
CREATE TABLE public.limit_classes (
id integer NOT NULL,
label VARCHAR(32),
backend_mode public.backend_mode,
environments integer DEFAULT '-1'::integer NOT NULL,
shares integer DEFAULT '-1'::integer NOT NULL,
reserved_shares integer DEFAULT '-1'::integer NOT NULL,
unique_names integer DEFAULT '-1'::integer NOT NULL,
share_frontends integer DEFAULT '-1'::integer NOT NULL,
period_minutes integer DEFAULT 1440 NOT NULL,
rx_bytes bigint DEFAULT '-1'::integer NOT NULL,
tx_bytes bigint DEFAULT '-1'::integer NOT NULL,
@ -130,7 +133,7 @@ Create a row in this table linking the `account_id` to the `limit_class_id` to a
To support overriding the resource count limits defined in the global limits configuration, a site administrator can create a limit class by inserting a row into the `limit_classes` table structured like this:
```sql
insert into limit_classes (environments, shares, reserved_shares, unique_names) values (1, 1, 1, 1);
insert into limit_classes (environments, shares, reserved_shares, unique_names, share_frontends) values (1, 1, 1, 1, 1);
```
This creates a limit class that sets the `environments`, `shares`, `reserved_shares`, and `unique_names` all to `1`.

View File

@ -124,6 +124,7 @@ func (t *FilesystemTarget) WriteStream(path string, stream io.Reader, mode os.Fi
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, stream)
if err != nil {
return err

View File

@ -13,6 +13,7 @@ type Root interface {
Client() (*rest_client_zrok.Zrok, error)
ApiEndpoint() (string, string)
DefaultFrontend() (string, string)
IsEnabled() bool
Environment() *Environment
@ -34,7 +35,8 @@ type Environment struct {
}
type Config struct {
ApiEndpoint string
ApiEndpoint string
DefaultFrontend string
}
type Metadata struct {

View File

@ -85,6 +85,24 @@ func (r *Root) ApiEndpoint() (string, string) {
return apiEndpoint, from
}
func (r *Root) DefaultFrontend() (string, string) {
defaultFrontend := "public"
from := "binary"
if r.Config() != nil && r.Config().DefaultFrontend != "" {
defaultFrontend = r.Config().DefaultFrontend
from = "config"
}
env := os.Getenv("ZROK_DEFAULT_FRONTEND")
if env != "" {
defaultFrontend = env
from = "ZROK_DEFAULT_FRONTEND"
}
return defaultFrontend, from
}
func (r *Root) Environment() *env_core.Environment {
return r.env
}

View File

@ -85,6 +85,24 @@ func (r *Root) ApiEndpoint() (string, string) {
return apiEndpoint, from
}
func (r *Root) DefaultFrontend() (string, string) {
defaultFrontend := "public"
from := "binary"
if r.Config() != nil && r.Config().DefaultFrontend != "" {
defaultFrontend = r.Config().DefaultFrontend
from = "config"
}
env := os.Getenv("ZROK_DEFAULT_FRONTEND")
if env != "" {
defaultFrontend = env
from = "ZROK_DEFAULT_FRONTEND"
}
return defaultFrontend, from
}
func (r *Root) Environment() *env_core.Environment {
return r.env
}

View File

@ -223,13 +223,14 @@ func loadConfig() (*env_core.Config, error) {
return nil, errors.Wrapf(err, "error unmarshaling config file '%v'", cf)
}
out := &env_core.Config{
ApiEndpoint: cfg.ApiEndpoint,
ApiEndpoint: cfg.ApiEndpoint,
DefaultFrontend: cfg.DefaultFrontend,
}
return out, nil
}
func saveConfig(cfg *env_core.Config) error {
in := &config{ApiEndpoint: cfg.ApiEndpoint}
in := &config{ApiEndpoint: cfg.ApiEndpoint, DefaultFrontend: cfg.DefaultFrontend}
data, err := json.MarshalIndent(in, "", " ")
if err != nil {
return errors.Wrap(err, "error marshaling config")
@ -323,7 +324,8 @@ type metadata struct {
}
type config struct {
ApiEndpoint string `json:"api_endpoint"`
ApiEndpoint string `json:"api_endpoint"`
DefaultFrontend string `json:"default_frontend"`
}
type environment struct {

View File

@ -83,6 +83,7 @@ limits:
shares: -1
reserved_shares: -1
unique_names: -1
share_frontends: -1
bandwidth:
period: 5m
warning:

View File

@ -7,9 +7,12 @@ package rest_model_zrok
import (
"context"
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// CreateFrontendRequest create frontend request
@ -17,6 +20,10 @@ import (
// swagger:model createFrontendRequest
type CreateFrontendRequest struct {
// permission mode
// Enum: [open closed]
PermissionMode string `json:"permissionMode,omitempty"`
// public name
PublicName string `json:"public_name,omitempty"`
@ -29,6 +36,57 @@ type CreateFrontendRequest struct {
// Validate validates this create frontend request
func (m *CreateFrontendRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validatePermissionMode(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var createFrontendRequestTypePermissionModePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["open","closed"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
createFrontendRequestTypePermissionModePropEnum = append(createFrontendRequestTypePermissionModePropEnum, v)
}
}
const (
// CreateFrontendRequestPermissionModeOpen captures enum value "open"
CreateFrontendRequestPermissionModeOpen string = "open"
// CreateFrontendRequestPermissionModeClosed captures enum value "closed"
CreateFrontendRequestPermissionModeClosed string = "closed"
)
// prop value enum
func (m *CreateFrontendRequest) validatePermissionModeEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, createFrontendRequestTypePermissionModePropEnum, true); err != nil {
return err
}
return nil
}
func (m *CreateFrontendRequest) validatePermissionMode(formats strfmt.Registry) error {
if swag.IsZero(m.PermissionMode) { // not required
return nil
}
// value enum
if err := m.validatePermissionModeEnum("permissionMode", "body", m.PermissionMode); err != nil {
return err
}
return nil
}

View File

@ -1200,6 +1200,13 @@ func init() {
"createFrontendRequest": {
"type": "object",
"properties": {
"permissionMode": {
"type": "string",
"enum": [
"open",
"closed"
]
},
"public_name": {
"type": "string"
},
@ -2956,6 +2963,13 @@ func init() {
"createFrontendRequest": {
"type": "object",
"properties": {
"permissionMode": {
"type": "string",
"enum": [
"open",
"closed"
]
},
"public_name": {
"type": "string"
},

View File

@ -9,8 +9,8 @@
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"@openziti/ziti-sdk-nodejs": "^0.16.0",
"axios": "^1.6.8",
"@openziti/ziti-sdk-nodejs": "^0.17.0",
"axios": "^1.7.2",
"express": "^4.19.2"
},
"devDependencies": {
@ -498,10 +498,11 @@
}
},
"node_modules/@openziti/ziti-sdk-nodejs": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@openziti/ziti-sdk-nodejs/-/ziti-sdk-nodejs-0.16.0.tgz",
"integrity": "sha512-jQG5Yn6XojfGXkVHliZReY48bq7P2fFWzyOtXw37GdTeo+RQRl9YS57ieRF70NrlL0oEkO1/84wSQBfpX+uj+A==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@openziti/ziti-sdk-nodejs/-/ziti-sdk-nodejs-0.17.0.tgz",
"integrity": "sha512-eufD2LxhRfB8yPUkUFStFJN4GAmLM8u2m0BKAwOdcYy7KTrgWpiDhE/tt2orCtTdd7F+opUSA590ubY48g9RNQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"bindings": "^1.5.0",
@ -771,9 +772,10 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -3518,9 +3520,9 @@
}
},
"@openziti/ziti-sdk-nodejs": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@openziti/ziti-sdk-nodejs/-/ziti-sdk-nodejs-0.16.0.tgz",
"integrity": "sha512-jQG5Yn6XojfGXkVHliZReY48bq7P2fFWzyOtXw37GdTeo+RQRl9YS57ieRF70NrlL0oEkO1/84wSQBfpX+uj+A==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@openziti/ziti-sdk-nodejs/-/ziti-sdk-nodejs-0.17.0.tgz",
"integrity": "sha512-eufD2LxhRfB8yPUkUFStFJN4GAmLM8u2m0BKAwOdcYy7KTrgWpiDhE/tt2orCtTdd7F+opUSA590ubY48g9RNQ==",
"requires": {
"@mapbox/node-pre-gyp": "^1.0.11",
"bindings": "^1.5.0",
@ -3754,9 +3756,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",

View File

@ -30,8 +30,8 @@
},
"homepage": "https://github.com/openziti/zrok#readme",
"dependencies": {
"@openziti/ziti-sdk-nodejs": "^0.16.0",
"axios": "^1.6.8",
"@openziti/ziti-sdk-nodejs": "^0.17.0",
"axios": "^1.7.2",
"express": "^4.19.2"
},
"exports": {

View File

@ -1 +1 @@
7.4.0
7.6.0

View File

@ -16,6 +16,7 @@ export class CreateFrontendRequest {
'zId'?: string;
'urlTemplate'?: string;
'publicName'?: string;
'permissionMode'?: CreateFrontendRequest.PermissionModeEnum;
static discriminator: string | undefined = undefined;
@ -34,6 +35,11 @@ export class CreateFrontendRequest {
"name": "publicName",
"baseName": "public_name",
"type": "string"
},
{
"name": "permissionMode",
"baseName": "permissionMode",
"type": "CreateFrontendRequest.PermissionModeEnum"
} ];
static getAttributeTypeMap() {
@ -41,3 +47,9 @@ export class CreateFrontendRequest {
}
}
export namespace CreateFrontendRequest {
export enum PermissionModeEnum {
Open = <any> 'open',
Closed = <any> 'closed'
}
}

View File

@ -108,6 +108,7 @@ let primitives = [
];
let enumsMap: {[index: string]: any} = {
"CreateFrontendRequest.PermissionModeEnum": CreateFrontendRequest.PermissionModeEnum,
"ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum,
"ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum,
"ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum,

View File

@ -120,7 +120,8 @@ export namespace ShareRequest {
UdpTunnel = <any> 'udpTunnel',
Caddy = <any> 'caddy',
Drive = <any> 'drive',
Socks = <any> 'socks'
Socks = <any> 'socks',
Vpn = <any> 'vpn'
}
export enum OauthProviderEnum {
Github = <any> 'github',

View File

@ -30,20 +30,23 @@ class CreateFrontendRequest(object):
swagger_types = {
'z_id': 'str',
'url_template': 'str',
'public_name': 'str'
'public_name': 'str',
'permission_mode': 'str'
}
attribute_map = {
'z_id': 'zId',
'url_template': 'url_template',
'public_name': 'public_name'
'public_name': 'public_name',
'permission_mode': 'permissionMode'
}
def __init__(self, z_id=None, url_template=None, public_name=None): # noqa: E501
def __init__(self, z_id=None, url_template=None, public_name=None, permission_mode=None): # noqa: E501
"""CreateFrontendRequest - a model defined in Swagger""" # noqa: E501
self._z_id = None
self._url_template = None
self._public_name = None
self._permission_mode = None
self.discriminator = None
if z_id is not None:
self.z_id = z_id
@ -51,6 +54,8 @@ class CreateFrontendRequest(object):
self.url_template = url_template
if public_name is not None:
self.public_name = public_name
if permission_mode is not None:
self.permission_mode = permission_mode
@property
def z_id(self):
@ -115,6 +120,33 @@ class CreateFrontendRequest(object):
self._public_name = public_name
@property
def permission_mode(self):
"""Gets the permission_mode of this CreateFrontendRequest. # noqa: E501
:return: The permission_mode of this CreateFrontendRequest. # noqa: E501
:rtype: str
"""
return self._permission_mode
@permission_mode.setter
def permission_mode(self, permission_mode):
"""Sets the permission_mode of this CreateFrontendRequest.
:param permission_mode: The permission_mode of this CreateFrontendRequest. # noqa: E501
:type: str
"""
allowed_values = ["open", "closed"] # noqa: E501
if permission_mode not in allowed_values:
raise ValueError(
"Invalid value for `permission_mode` ({0}), must be one of {1}" # noqa: E501
.format(permission_mode, allowed_values)
)
self._permission_mode = permission_mode
def to_dict(self):
"""Returns the model properties as a dict"""
result = {}

View File

@ -766,6 +766,9 @@ definitions:
type: string
public_name:
type: string
permissionMode:
type: string
enum: ["open", "closed"]
createFrontendResponse:
type: object

1002
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,20 @@
"@emotion/styled": "^11.10.4",
"@mdi/js": "^7.0.96",
"@mdi/react": "^1.6.1",
"@mui/material": "^5.10.4",
"@mui/material": "^5.15.18",
"bootstrap": "^5.2.3",
"dagre": "^0.8.5",
"eslint-config-react-app": "^7.0.1",
"humanize-duration": "^3.27.3",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.2",
"react-data-table-component": "^7.5.2",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-force-graph": "^1.43.0",
"react-router-dom": "^6.4.0",
"react-router-dom": "^6.23.1",
"react-sizeme": "^3.0.2",
"recharts": "^2.6.1",
"recharts": "^2.12.7",
"styled-components": "^5.3.5",
"svgo": "^3.0.2"
},

View File

@ -53,6 +53,7 @@
* @property {string} zId
* @property {string} url_template
* @property {string} public_name
* @property {string} permissionMode
*/
/**

View File

@ -14,7 +14,7 @@ const ActionsTab = (props) => {
return (
<div className={"actions-tab"}>
<div id={"change-password"} style={{"padding-top": "10px"}}>
<div id={"change-password"} style={{"paddingTop": "10px"}}>
<h3>Change Password?</h3>
<p>Change the password used to log into the zrok web console.</p>
<Button variant={"danger"} onClick={openChangePasswordModal}>Change Password</Button>

View File

@ -11,12 +11,12 @@
"@docusaurus/core": "^3.3.2",
"@docusaurus/plugin-client-redirects": "^3.3.2",
"@docusaurus/preset-classic": "^3.3.2",
"@mdx-js/react": "^3.0.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react": "^18.3.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"remark-math": "^5.1.1"
},
"devDependencies": {
@ -2962,9 +2962,10 @@
}
},
"node_modules/@mdx-js/react": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.0.tgz",
"integrity": "sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
"integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==",
"license": "MIT",
"dependencies": {
"@types/mdx": "^2.0.0"
},
@ -12033,9 +12034,10 @@
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -12174,15 +12176,16 @@
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.2.0"
"react": "^18.3.1"
}
},
"node_modules/react-error-overlay": {
@ -12948,9 +12951,10 @@
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}

View File

@ -17,12 +17,12 @@
"@docusaurus/core": "^3.3.2",
"@docusaurus/plugin-client-redirects": "^3.3.2",
"@docusaurus/preset-classic": "^3.3.2",
"@mdx-js/react": "^3.0.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react": "^18.3.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"remark-math": "^5.1.1"
},
"devDependencies": {