mirror of
https://github.com/vx3r/wg-gen-web.git
synced 2024-11-22 02:31:57 +01:00
refactor, remove dirty migration, npm update
This commit is contained in:
parent
9e4b22d0ef
commit
a765dead02
@ -7,7 +7,7 @@ build-back:
|
|||||||
stage: build artifacts
|
stage: build artifacts
|
||||||
image: golang:latest
|
image: golang:latest
|
||||||
script:
|
script:
|
||||||
- GOOS=linux GOARCH=amd64 go build -ldflags="-X 'gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util.Version=${CI_COMMIT_SHA}'" -o ${CI_PROJECT_NAME}-linux-amd64
|
- GOOS=linux GOARCH=amd64 go build -ldflags="-X 'gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version.Version=${CI_COMMIT_SHA}'" gitlab.127-0-0-1.fr/vx3r/wg-gen-web/cmd/wg-gen-web -o ${CI_PROJECT_NAME}-linux-amd64
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- ${CI_PROJECT_NAME}-linux-amd64
|
- ${CI_PROJECT_NAME}-linux-amd64
|
||||||
|
@ -4,7 +4,7 @@ FROM golang:alpine AS build-back
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG COMMIT
|
ARG COMMIT
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build -ldflags="-X 'gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util.Version=${COMMIT}'" -o wg-gen-web-linux
|
RUN go build -ldflags="-X 'gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version.Version=${COMMIT}'" gitlab.127-0-0-1.fr/vx3r/wg-gen-web/cmd/wg-gen-web -o wg-gen-web-linux
|
||||||
|
|
||||||
FROM node:10-alpine AS build-front
|
FROM node:10-alpine AS build-front
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ func ApplyRoutes(r *gin.Engine) {
|
|||||||
server.GET("", readServer)
|
server.GET("", readServer)
|
||||||
server.PATCH("", updateServer)
|
server.PATCH("", updateServer)
|
||||||
server.GET("/config", configServer)
|
server.GET("/config", configServer)
|
||||||
server.GET("/version", version)
|
server.GET("/version", versionStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,8 +238,8 @@ func configServer(c *gin.Context) {
|
|||||||
c.Data(http.StatusOK, "application/config", configData)
|
c.Data(http.StatusOK, "application/config", configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func version(c *gin.Context) {
|
func versionStr(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"version": util.Version,
|
"version": version.Version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@ -22,7 +23,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Infof("Starting Wg Gen Web version: %s", util.Version)
|
log.Infof("Starting Wg Gen Web version: %s", version.Version)
|
||||||
|
|
||||||
// load .env environment variables
|
// load .env environment variables
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
@ -65,20 +66,6 @@ func main() {
|
|||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate
|
|
||||||
err = core.MigrateInitialStructChange()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Fatal("failed to migrate initial struct changes")
|
|
||||||
}
|
|
||||||
err = core.MigratePresharedKey()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Fatal("failed to migrate preshared key struct changes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dump wg config file
|
// dump wg config file
|
||||||
err = core.UpdateServerConfigWg()
|
err = core.UpdateServerConfigWg()
|
||||||
if err != nil {
|
if err != nil {
|
@ -203,7 +203,7 @@ func ReadClientConfig(id string) ([]byte, error) {
|
|||||||
return configDataWg, nil
|
return configDataWg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEmail to client
|
// EmailClient send email to client
|
||||||
func EmailClient(id string) error {
|
func EmailClient(id string) error {
|
||||||
client, err := ReadClient(id)
|
client, err := ReadClient(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
309
core/migrate.go
309
core/migrate.go
@ -1,309 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
uuid "github.com/satori/go.uuid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
|
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Migrate all changes, current struct fields change
|
|
||||||
func MigrateInitialStructChange() error {
|
|
||||||
clients, err := readClients()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := deserialize("server.json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
switch v := client["allowedIPs"].(type) {
|
|
||||||
case []interface{}:
|
|
||||||
log.Infof("client %s has been already migrated", client["id"])
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
log.Infof("unexpected type %T, mus be migrated", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &model.Client{}
|
|
||||||
c.Id = client["id"].(string)
|
|
||||||
c.Name = client["name"].(string)
|
|
||||||
c.Email = client["email"].(string)
|
|
||||||
c.Enable = client["enable"].(bool)
|
|
||||||
c.AllowedIPs = make([]string, 0)
|
|
||||||
for _, address := range strings.Split(client["allowedIPs"].(string), ",") {
|
|
||||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
|
||||||
c.AllowedIPs = append(c.AllowedIPs, strings.TrimSpace(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Address = make([]string, 0)
|
|
||||||
for _, address := range strings.Split(client["address"].(string), ",") {
|
|
||||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
|
||||||
c.Address = append(c.Address, strings.TrimSpace(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.PrivateKey = client["privateKey"].(string)
|
|
||||||
c.PublicKey = client["publicKey"].(string)
|
|
||||||
created, err := time.Parse(time.RFC3339, client["created"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.Created = created
|
|
||||||
updated, err := time.Parse(time.RFC3339, client["updated"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.Updated = updated
|
|
||||||
|
|
||||||
err = storage.Serialize(c.Id, c)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to Serialize client")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := s["address"].(type) {
|
|
||||||
case []interface{}:
|
|
||||||
log.Info("server has been already migrated")
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
log.Infof("unexpected type %T, mus be migrated", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &model.Server{}
|
|
||||||
|
|
||||||
server.Address = make([]string, 0)
|
|
||||||
for _, address := range strings.Split(s["address"].(string), ",") {
|
|
||||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
|
||||||
server.Address = append(server.Address, strings.TrimSpace(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
server.ListenPort = int(s["listenPort"].(float64))
|
|
||||||
server.PrivateKey = s["privateKey"].(string)
|
|
||||||
server.PublicKey = s["publicKey"].(string)
|
|
||||||
//server.PresharedKey = s["presharedKey"].(string)
|
|
||||||
server.Endpoint = s["endpoint"].(string)
|
|
||||||
server.PersistentKeepalive = int(s["persistentKeepalive"].(float64))
|
|
||||||
server.Dns = make([]string, 0)
|
|
||||||
for _, address := range strings.Split(s["dns"].(string), ",") {
|
|
||||||
if util.IsValidIp(strings.TrimSpace(address)) {
|
|
||||||
server.Dns = append(server.Dns, strings.TrimSpace(address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if val, ok := s["preUp"]; ok {
|
|
||||||
server.PreUp = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["postUp"]; ok {
|
|
||||||
server.PostUp = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["preDown"]; ok {
|
|
||||||
server.PreDown = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["postDown"]; ok {
|
|
||||||
server.PostDown = val.(string)
|
|
||||||
}
|
|
||||||
created, err := time.Parse(time.RFC3339, s["created"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
}
|
|
||||||
server.Created = created
|
|
||||||
updated, err := time.Parse(time.RFC3339, s["updated"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
}
|
|
||||||
server.Updated = updated
|
|
||||||
|
|
||||||
err = storage.Serialize("server.json", server)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to Serialize server")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate presharedKey issue #23
|
|
||||||
func MigratePresharedKey() error {
|
|
||||||
clients, err := readClients()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := deserialize("server.json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
if _, ok := client["presharedKey"]; ok {
|
|
||||||
log.Infof("client %s has been already migrated for preshared key", client["id"])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &model.Client{}
|
|
||||||
c.Id = client["id"].(string)
|
|
||||||
c.Name = client["name"].(string)
|
|
||||||
c.Email = client["email"].(string)
|
|
||||||
c.Enable = client["enable"].(bool)
|
|
||||||
if val, ok := client["ignorePersistentKeepalive"]; ok {
|
|
||||||
c.IgnorePersistentKeepalive = val.(bool)
|
|
||||||
} else {
|
|
||||||
c.IgnorePersistentKeepalive = false
|
|
||||||
}
|
|
||||||
c.PresharedKey = s["presharedKey"].(string)
|
|
||||||
c.AllowedIPs = make([]string, 0)
|
|
||||||
for _, address := range client["allowedIPs"].([]interface{}) {
|
|
||||||
c.AllowedIPs = append(c.AllowedIPs, address.(string))
|
|
||||||
}
|
|
||||||
c.Address = make([]string, 0)
|
|
||||||
for _, address := range client["address"].([]interface{}) {
|
|
||||||
c.Address = append(c.Address, address.(string))
|
|
||||||
}
|
|
||||||
c.PrivateKey = client["privateKey"].(string)
|
|
||||||
c.PublicKey = client["publicKey"].(string)
|
|
||||||
created, err := time.Parse(time.RFC3339, client["created"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.Created = created
|
|
||||||
updated, err := time.Parse(time.RFC3339, client["updated"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.Updated = updated
|
|
||||||
|
|
||||||
err = storage.Serialize(c.Id, c)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to Serialize client")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := s["presharedKey"]; ok {
|
|
||||||
server := &model.Server{}
|
|
||||||
|
|
||||||
server.Address = make([]string, 0)
|
|
||||||
server.Address = make([]string, 0)
|
|
||||||
for _, address := range s["address"].([]interface{}) {
|
|
||||||
server.Address = append(server.Address, address.(string))
|
|
||||||
}
|
|
||||||
server.ListenPort = int(s["listenPort"].(float64))
|
|
||||||
server.PrivateKey = s["privateKey"].(string)
|
|
||||||
server.PublicKey = s["publicKey"].(string)
|
|
||||||
server.Endpoint = s["endpoint"].(string)
|
|
||||||
server.PersistentKeepalive = int(s["persistentKeepalive"].(float64))
|
|
||||||
server.Dns = make([]string, 0)
|
|
||||||
for _, address := range s["dns"].([]interface{}) {
|
|
||||||
server.Dns = append(server.Dns, address.(string))
|
|
||||||
}
|
|
||||||
if val, ok := s["preUp"]; ok {
|
|
||||||
server.PreUp = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["postUp"]; ok {
|
|
||||||
server.PostUp = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["preDown"]; ok {
|
|
||||||
server.PreDown = val.(string)
|
|
||||||
}
|
|
||||||
if val, ok := s["postDown"]; ok {
|
|
||||||
server.PostDown = val.(string)
|
|
||||||
}
|
|
||||||
created, err := time.Parse(time.RFC3339, s["created"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
}
|
|
||||||
server.Created = created
|
|
||||||
updated, err := time.Parse(time.RFC3339, s["updated"].(string))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to parse time")
|
|
||||||
}
|
|
||||||
server.Updated = updated
|
|
||||||
|
|
||||||
err = storage.Serialize("server.json", server)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
}).Errorf("failed to Serialize server")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readClients() ([]map[string]interface{}, error) {
|
|
||||||
clients := make([]map[string]interface{}, 0)
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR")))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
// clients file name is an uuid
|
|
||||||
_, err := uuid.FromString(f.Name())
|
|
||||||
if err == nil {
|
|
||||||
c, err := deserialize(f.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"err": err,
|
|
||||||
"path": f.Name(),
|
|
||||||
}).Error("failed to deserialize client")
|
|
||||||
} else {
|
|
||||||
clients = append(clients, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clients, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deserialize(id string) (map[string]interface{}, error) {
|
|
||||||
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
|
|
||||||
|
|
||||||
data, err := util.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var d map[string]interface{}
|
|
||||||
err = json.Unmarshal(data, &d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
@ -22,6 +22,7 @@ type Client struct {
|
|||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid check if model is valid
|
||||||
func (a Client) IsValid() []error {
|
func (a Client) IsValid() []error {
|
||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ type Server struct {
|
|||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid check if model is valid
|
||||||
func (a Server) IsValid() []error {
|
func (a Server) IsValid() []error {
|
||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
|
|
||||||
|
1476
ui/package-lock.json
generated
1476
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,11 +13,11 @@
|
|||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-moment": "^4.1.0",
|
"vue-moment": "^4.1.0",
|
||||||
"vue-router": "^3.1.6",
|
"vue-router": "^3.1.6",
|
||||||
"vuetify": "^2.2.18"
|
"vuetify": "^2.2.22"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-router": "^4.2.3",
|
"@vue/cli-plugin-router": "^4.3.1",
|
||||||
"@vue/cli-service": "^4.2.3",
|
"@vue/cli-service": "^4.3.1",
|
||||||
"sass": "^1.26.3",
|
"sass": "^1.26.3",
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^8.0.0",
|
||||||
"vue-cli-plugin-vuetify": "^2.0.5",
|
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||||
|
@ -9,9 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// RegexpEmail check valid email
|
||||||
RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||||
// pushed build time
|
|
||||||
Version string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReadFile file content
|
// ReadFile file content
|
||||||
|
4
version/version.go
Normal file
4
version/version.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
// Version build time set version
|
||||||
|
var Version = "_ci_build_not_run_properly_"
|
Loading…
Reference in New Issue
Block a user