mirror of
https://github.com/openziti/zrok.git
synced 2024-11-21 15:43:22 +01:00
adjust admin tooling to support creating open/closed permission mode frontends (#539)
This commit is contained in:
parent
c2878dcd85
commit
49368dc542
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/openziti/zrok/environment"
|
"github.com/openziti/zrok/environment"
|
||||||
"github.com/openziti/zrok/rest_client_zrok/admin"
|
"github.com/openziti/zrok/rest_client_zrok/admin"
|
||||||
"github.com/openziti/zrok/rest_model_zrok"
|
"github.com/openziti/zrok/rest_model_zrok"
|
||||||
|
"github.com/openziti/zrok/sdk/golang/sdk"
|
||||||
"github.com/openziti/zrok/tui"
|
"github.com/openziti/zrok/tui"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -15,7 +16,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type adminCreateFrontendCommand struct {
|
type adminCreateFrontendCommand struct {
|
||||||
cmd *cobra.Command
|
cmd *cobra.Command
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
|
func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
|
||||||
@ -25,6 +27,7 @@ func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
|
|||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
}
|
}
|
||||||
command := &adminCreateFrontendCommand{cmd: cmd}
|
command := &adminCreateFrontendCommand{cmd: cmd}
|
||||||
|
cmd.Flags().BoolVar(&command.closed, "closed", false, "Enabled closed permission mode")
|
||||||
cmd.Run = command.run
|
cmd.Run = command.run
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
@ -44,11 +47,16 @@ func (cmd *adminCreateFrontendCommand) run(_ *cobra.Command, args []string) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
permissionMode := sdk.OpenPermissionMode
|
||||||
|
if cmd.closed {
|
||||||
|
permissionMode = sdk.ClosedPermissionMode
|
||||||
|
}
|
||||||
req := admin.NewCreateFrontendParams()
|
req := admin.NewCreateFrontendParams()
|
||||||
req.Body = &rest_model_zrok.CreateFrontendRequest{
|
req.Body = &rest_model_zrok.CreateFrontendRequest{
|
||||||
ZID: zId,
|
ZID: zId,
|
||||||
PublicName: publicName,
|
PublicName: publicName,
|
||||||
URLTemplate: urlTemplate,
|
URLTemplate: urlTemplate,
|
||||||
|
PermissionMode: string(permissionMode),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth())
|
resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth())
|
||||||
|
@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
@ -57,11 +56,12 @@ func (h *createFrontendHandler) Handle(params admin.CreateFrontendParams, princi
|
|||||||
}
|
}
|
||||||
|
|
||||||
fe := &store.Frontend{
|
fe := &store.Frontend{
|
||||||
Token: feToken,
|
Token: feToken,
|
||||||
ZId: params.Body.ZID,
|
ZId: params.Body.ZID,
|
||||||
PublicName: ¶ms.Body.PublicName,
|
PublicName: ¶ms.Body.PublicName,
|
||||||
UrlTemplate: ¶ms.Body.URLTemplate,
|
UrlTemplate: ¶ms.Body.URLTemplate,
|
||||||
Reserved: true,
|
Reserved: true,
|
||||||
|
PermissionMode: store.PermissionMode(params.Body.PermissionMode),
|
||||||
}
|
}
|
||||||
if _, err := str.CreateGlobalFrontend(fe, tx); err != nil {
|
if _, err := str.CreateGlobalFrontend(fe, tx); err != nil {
|
||||||
perr := &pq.Error{}
|
perr := &pq.Error{}
|
||||||
|
@ -14,28 +14,28 @@ type Frontend struct {
|
|||||||
PublicName *string
|
PublicName *string
|
||||||
UrlTemplate *string
|
UrlTemplate *string
|
||||||
Reserved bool
|
Reserved bool
|
||||||
Deleted bool
|
PermissionMode PermissionMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) {
|
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 {
|
if err != nil {
|
||||||
return 0, errors.Wrap(err, "error preparing frontends insert statement")
|
return 0, errors.Wrap(err, "error preparing frontends insert statement")
|
||||||
}
|
}
|
||||||
var id int
|
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 0, errors.Wrap(err, "error executing frontends insert statement")
|
||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) {
|
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 {
|
if err != nil {
|
||||||
return 0, errors.Wrap(err, "error preparing global frontends insert statement")
|
return 0, errors.Wrap(err, "error preparing global frontends insert statement")
|
||||||
}
|
}
|
||||||
var id int
|
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 0, errors.Wrap(err, "error executing global frontends insert statement")
|
||||||
}
|
}
|
||||||
return id, nil
|
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 {
|
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)
|
stmt, err := tx.Prepare(sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error preparing frontends update statement")
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error executing frontends update statement")
|
return errors.Wrap(err, "error executing frontends update statement")
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ type Share struct {
|
|||||||
Reserved bool
|
Reserved bool
|
||||||
UniqueName bool
|
UniqueName bool
|
||||||
PermissionMode PermissionMode
|
PermissionMode PermissionMode
|
||||||
Deleted bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) {
|
func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
-- +migrate Up
|
-- +migrate Up
|
||||||
|
|
||||||
|
alter table frontends add column permission_mode permission_mode_type not null default('open');
|
||||||
|
|
||||||
create table frontend_grants (
|
create table frontend_grants (
|
||||||
id serial primary key,
|
id serial primary key,
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
-- +migrate Up
|
-- +migrate Up
|
||||||
|
|
||||||
|
alter table frontends add column permission_mode string not null default('open');
|
||||||
|
|
||||||
create table frontend_grants (
|
create table frontend_grants (
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
|
|
||||||
|
@ -7,9 +7,12 @@ package rest_model_zrok
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/go-openapi/errors"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/go-openapi/swag"
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/go-openapi/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateFrontendRequest create frontend request
|
// CreateFrontendRequest create frontend request
|
||||||
@ -17,6 +20,10 @@ import (
|
|||||||
// swagger:model createFrontendRequest
|
// swagger:model createFrontendRequest
|
||||||
type CreateFrontendRequest struct {
|
type CreateFrontendRequest struct {
|
||||||
|
|
||||||
|
// permission mode
|
||||||
|
// Enum: [open closed]
|
||||||
|
PermissionMode string `json:"permissionMode,omitempty"`
|
||||||
|
|
||||||
// public name
|
// public name
|
||||||
PublicName string `json:"public_name,omitempty"`
|
PublicName string `json:"public_name,omitempty"`
|
||||||
|
|
||||||
@ -29,6 +36,57 @@ type CreateFrontendRequest struct {
|
|||||||
|
|
||||||
// Validate validates this create frontend request
|
// Validate validates this create frontend request
|
||||||
func (m *CreateFrontendRequest) Validate(formats strfmt.Registry) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1200,6 +1200,13 @@ func init() {
|
|||||||
"createFrontendRequest": {
|
"createFrontendRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"permissionMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"open",
|
||||||
|
"closed"
|
||||||
|
]
|
||||||
|
},
|
||||||
"public_name": {
|
"public_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -2956,6 +2963,13 @@ func init() {
|
|||||||
"createFrontendRequest": {
|
"createFrontendRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"permissionMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"open",
|
||||||
|
"closed"
|
||||||
|
]
|
||||||
|
},
|
||||||
"public_name": {
|
"public_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -1 +1 @@
|
|||||||
7.4.0
|
7.6.0
|
||||||
|
@ -16,6 +16,7 @@ export class CreateFrontendRequest {
|
|||||||
'zId'?: string;
|
'zId'?: string;
|
||||||
'urlTemplate'?: string;
|
'urlTemplate'?: string;
|
||||||
'publicName'?: string;
|
'publicName'?: string;
|
||||||
|
'permissionMode'?: CreateFrontendRequest.PermissionModeEnum;
|
||||||
|
|
||||||
static discriminator: string | undefined = undefined;
|
static discriminator: string | undefined = undefined;
|
||||||
|
|
||||||
@ -34,6 +35,11 @@ export class CreateFrontendRequest {
|
|||||||
"name": "publicName",
|
"name": "publicName",
|
||||||
"baseName": "public_name",
|
"baseName": "public_name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "permissionMode",
|
||||||
|
"baseName": "permissionMode",
|
||||||
|
"type": "CreateFrontendRequest.PermissionModeEnum"
|
||||||
} ];
|
} ];
|
||||||
|
|
||||||
static getAttributeTypeMap() {
|
static getAttributeTypeMap() {
|
||||||
@ -41,3 +47,9 @@ export class CreateFrontendRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace CreateFrontendRequest {
|
||||||
|
export enum PermissionModeEnum {
|
||||||
|
Open = <any> 'open',
|
||||||
|
Closed = <any> 'closed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -108,6 +108,7 @@ let primitives = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
let enumsMap: {[index: string]: any} = {
|
let enumsMap: {[index: string]: any} = {
|
||||||
|
"CreateFrontendRequest.PermissionModeEnum": CreateFrontendRequest.PermissionModeEnum,
|
||||||
"ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum,
|
"ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum,
|
||||||
"ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum,
|
"ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum,
|
||||||
"ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum,
|
"ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum,
|
||||||
|
@ -120,7 +120,8 @@ export namespace ShareRequest {
|
|||||||
UdpTunnel = <any> 'udpTunnel',
|
UdpTunnel = <any> 'udpTunnel',
|
||||||
Caddy = <any> 'caddy',
|
Caddy = <any> 'caddy',
|
||||||
Drive = <any> 'drive',
|
Drive = <any> 'drive',
|
||||||
Socks = <any> 'socks'
|
Socks = <any> 'socks',
|
||||||
|
Vpn = <any> 'vpn'
|
||||||
}
|
}
|
||||||
export enum OauthProviderEnum {
|
export enum OauthProviderEnum {
|
||||||
Github = <any> 'github',
|
Github = <any> 'github',
|
||||||
|
@ -30,20 +30,23 @@ class CreateFrontendRequest(object):
|
|||||||
swagger_types = {
|
swagger_types = {
|
||||||
'z_id': 'str',
|
'z_id': 'str',
|
||||||
'url_template': 'str',
|
'url_template': 'str',
|
||||||
'public_name': 'str'
|
'public_name': 'str',
|
||||||
|
'permission_mode': 'str'
|
||||||
}
|
}
|
||||||
|
|
||||||
attribute_map = {
|
attribute_map = {
|
||||||
'z_id': 'zId',
|
'z_id': 'zId',
|
||||||
'url_template': 'url_template',
|
'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
|
"""CreateFrontendRequest - a model defined in Swagger""" # noqa: E501
|
||||||
self._z_id = None
|
self._z_id = None
|
||||||
self._url_template = None
|
self._url_template = None
|
||||||
self._public_name = None
|
self._public_name = None
|
||||||
|
self._permission_mode = None
|
||||||
self.discriminator = None
|
self.discriminator = None
|
||||||
if z_id is not None:
|
if z_id is not None:
|
||||||
self.z_id = z_id
|
self.z_id = z_id
|
||||||
@ -51,6 +54,8 @@ class CreateFrontendRequest(object):
|
|||||||
self.url_template = url_template
|
self.url_template = url_template
|
||||||
if public_name is not None:
|
if public_name is not None:
|
||||||
self.public_name = public_name
|
self.public_name = public_name
|
||||||
|
if permission_mode is not None:
|
||||||
|
self.permission_mode = permission_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def z_id(self):
|
def z_id(self):
|
||||||
@ -115,6 +120,33 @@ class CreateFrontendRequest(object):
|
|||||||
|
|
||||||
self._public_name = public_name
|
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):
|
def to_dict(self):
|
||||||
"""Returns the model properties as a dict"""
|
"""Returns the model properties as a dict"""
|
||||||
result = {}
|
result = {}
|
||||||
|
@ -766,6 +766,9 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
public_name:
|
public_name:
|
||||||
type: string
|
type: string
|
||||||
|
permissionMode:
|
||||||
|
type: string
|
||||||
|
enum: ["open", "closed"]
|
||||||
|
|
||||||
createFrontendResponse:
|
createFrontendResponse:
|
||||||
type: object
|
type: object
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
* @property {string} zId
|
* @property {string} zId
|
||||||
* @property {string} url_template
|
* @property {string} url_template
|
||||||
* @property {string} public_name
|
* @property {string} public_name
|
||||||
|
* @property {string} permissionMode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user