Compare commits

...

No commits in common. "d90e2343a35ab237650bbc76bd66593c1929cf3a" and "f7b6277f0031d2a39fd3fc88ea3a986034737d58" have entirely different histories.

121 changed files with 25100 additions and 7 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.go linguist-detectable=true
*.js linguist-detectable=false

19
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Build
on: [push, pull_request]
jobs:
frontend-linter:
name: Frontend Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
# cache
- uses: c-hive/gha-yarn-cache@v2
with:
directory: ./web
- run: yarn install && yarn run lint
working-directory: ./web

14
.gitignore vendored
View File

@ -1,6 +1,3 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
@ -17,5 +14,12 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
.idea/
*.iml
tmp/
tmpFiles/
*.tmp
logs/
lastupdate.tmp
commentsRouter*.go

View File

@ -1,2 +1,84 @@
# casbase
Casbin Database
<h1 align="center" style="border-bottom: none;">📦⚡️ CasBase</h1>
<h3 align="center">An open-source AI knowledge base platform developed by Go and React.</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
<a href="https://hub.docker.com/r/casbin/casnode">
<img alt="docker pull casbin/casnode" src="https://img.shields.io/docker/pulls/casbin/casnode.svg">
</a>
<a href="https://github.com/casbin/casnode/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casbin/jcasbin/workflows/build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casnode.svg">
</a>
<a href="https://hub.docker.com/repository/docker/casbin/casnode">
<img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen">
</a>
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/casbin/casnode">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casbin/casnode?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/casbin/casnode?style=flat-square" alt="license">
</a>
<a href="https://github.com/casbin/casnode/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casbin/casnode?style=flat-square">
</a>
<a href="#">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casbin/casnode?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/network">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casbin/casnode?style=flat-square">
</a>
<a href="https://crowdin.com/project/casnode">
<img alt="Crowdin" src="https://badges.crowdin.net/casnode/localized.svg">
</a>
</p>
## Architecture
CasBase contains 2 parts:
| Name | Description | Language |
|----------|--------------------------------|------------------------|
| Frontend | Web frontend UI for CasBase | Javascript + React |
| Backend | RESTful API backend for CasBase | Golang + Beego + MySQL |
## Installation
CasBase uses Casdoor to manage members. So you need to create an organization and an application for CasBase in a Casdoor instance.
### Necessary configuration
#### Setup database
CasBase will store its users, nodes and topics informations in a MySQL database named: `casbase`, will create it if not existed. The DB connection string can be specified at: https://github.com/casbin/casbase/blob/master/conf/app.conf
```ini
dataSourceName = root:123@tcp(localhost:3306)/
```
CasBase uses XORM to connect to DB, so all DBs supported by XORM can also be used.
#### Run casbase
- Configure and run casbase by yourself. If you want to learn more about casbase, you see [casbase installation](https://casbase.org/docs/installation).
- Install casbase using docker. you see [installation by docker](https://casbase.org/docs/Docker).
- Install casbase using BTpanel. you see [installation by BTpanel](https://casbase.org/docs/BTpanel).
- Open browser:
http://localhost:3000/
### Optional configuration
#### Setup your platform to enable some third-party login platform
CasBase uses Casdoor to manage members. If you want to log in with oauth, you should see [casdoor oauth configuration](https://casdoor.org/docs/provider/OAuth).
#### OSS, Mail, and SMS services
CasBase uses Casdoor to upload files to cloud storage, send Emails and send SMSs. See Casdoor for more details.

89
casdoor/adapter.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package casdoor
import (
"runtime"
"github.com/astaxie/beego"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
var adapter *Adapter = nil
var CasdoorOrganization string
type Session struct {
SessionKey string `xorm:"char(64) notnull pk"`
SessionData []uint8 `xorm:"blob"`
SessionExpiry int `xorm:"notnull"`
}
func InitCasdoorAdapter() {
casdoorDbName := beego.AppConfig.String("casdoorDbName")
if casdoorDbName == "" {
return
}
adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), beego.AppConfig.String("casdoorDbName"))
CasdoorOrganization = beego.AppConfig.String("casdoorOrganization")
}
// Adapter represents the MySQL adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
dbName string
Engine *xorm.Engine
}
// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
err := a.Engine.Close()
if err != nil {
panic(err)
}
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
a.dbName = dbName
// Open the DB, create it if not existed.
a.open()
// Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer)
return a
}
func (a *Adapter) open() {
Engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName)
if err != nil {
panic(err)
}
a.Engine = Engine
}
func (a *Adapter) close() {
a.Engine.Close()
a.Engine = nil
}

73
casdoor/user_adapter.go Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package casdoor
import "github.com/casdoor/casdoor-go-sdk/auth"
func GetUsers() []*auth.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
users := []*auth.User{}
err := adapter.Engine.Desc("created_time").Find(&users, &auth.User{Owner: owner})
if err != nil {
panic(err)
}
return users
}
func GetSortedUsers(sorter string, limit int) []*auth.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
users := []*auth.User{}
err := adapter.Engine.Desc(sorter).Limit(limit, 0).Find(&users, &auth.User{Owner: owner})
if err != nil {
panic(err)
}
return users
}
func GetUser(name string) *auth.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
if owner == "" || name == "" {
return nil
}
user := auth.User{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}

19
conf/app.conf Normal file
View File

@ -0,0 +1,19 @@
appname = confita
httpport = 12000
runmode = dev
SessionOn = true
redisEndpoint =
copyrequestbody = true
socks5Proxy = "127.0.0.1:10808"
driverName = mysql
dataSourceName = root:123@tcp(localhost:3306)/
dbName = confita
casdoorEndpoint = http://localhost:8000
clientId = 4204b22726f5ff8c9efe
clientSecret = xxx
jwtSecret = CasdoorSecret
casdoorOrganization = "casbin"
casdoorApplication = "app-confita"
casdoorDbName =
cacheExpireSeconds = 60
chromeCtxNum = 1

28
conf/conf.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conf
import (
"os"
"github.com/astaxie/beego"
)
func GetConfigString(key string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return beego.AppConfig.String(key)
}

89
controllers/account.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
_ "embed"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/auth"
)
//go:embed token_jwt_key.pem
var JwtPublicKey string
func init() {
InitAuthConfig()
}
func InitAuthConfig() {
casdoorEndpoint := beego.AppConfig.String("casdoorEndpoint")
clientId := beego.AppConfig.String("clientId")
clientSecret := beego.AppConfig.String("clientSecret")
casdoorOrganization := beego.AppConfig.String("casdoorOrganization")
casdoorApplication := beego.AppConfig.String("casdoorApplication")
auth.InitConfig(casdoorEndpoint, clientId, clientSecret, JwtPublicKey, casdoorOrganization, casdoorApplication)
}
func (c *ApiController) Signin() {
code := c.Input().Get("code")
state := c.Input().Get("state")
token, err := auth.GetOAuthToken(code, state)
if err != nil {
c.ResponseError(err.Error())
return
}
claims, err := auth.ParseJwtToken(token.AccessToken)
if err != nil {
c.ResponseError(err.Error())
return
}
claims.AccessToken = token.AccessToken
c.SetSessionClaims(claims)
c.ResponseOk(claims)
}
func (c *ApiController) Signout() {
claims := c.GetSessionClaims()
if claims != nil {
clearUserDuplicated(claims)
}
c.SetSessionClaims(nil)
c.ResponseOk()
}
func (c *ApiController) GetAccount() {
if c.RequireSignedIn() {
return
}
claims := c.GetSessionClaims()
if isUserDuplicated(claims) {
if !claims.IsAdmin {
c.ResponseError("you have signed in from another place, this session has been ended")
return
}
}
c.ResponseOk(claims)
}

88
controllers/base.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/gob"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/auth"
)
type ApiController struct {
beego.Controller
}
func init() {
gob.Register(auth.Claims{})
}
func GetUserName(user *auth.User) string {
if user == nil {
return ""
}
return user.Name
}
func (c *ApiController) GetSessionClaims() *auth.Claims {
s := c.GetSession("user")
if s == nil {
return nil
}
claims := s.(auth.Claims)
return &claims
}
func (c *ApiController) SetSessionClaims(claims *auth.Claims) {
if claims == nil {
c.DelSession("user")
return
}
c.SetSession("user", *claims)
}
func (c *ApiController) GetSessionUser() *auth.User {
claims := c.GetSessionClaims()
if claims == nil {
return nil
}
return &claims.User
}
func (c *ApiController) SetSessionUser(user *auth.User) {
if user == nil {
c.DelSession("user")
return
}
claims := c.GetSessionClaims()
if claims != nil {
claims.User = *user
c.SetSessionClaims(claims)
}
}
func (c *ApiController) GetSessionUsername() string {
user := c.GetSessionUser()
if user == nil {
return ""
}
return GetUserName(user)
}

75
controllers/code.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/confita/object"
)
func (c *ApiController) GetGlobalCodes() {
c.Data["json"] = object.GetGlobalCodes()
c.ServeJSON()
}
func (c *ApiController) GetCodes() {
owner := c.Input().Get("owner")
c.Data["json"] = object.GetCodes(owner)
c.ServeJSON()
}
func (c *ApiController) GetCode() {
id := c.Input().Get("id")
c.Data["json"] = object.GetCode(id)
c.ServeJSON()
}
func (c *ApiController) UpdateCode() {
id := c.Input().Get("id")
var code object.Code
err := json.Unmarshal(c.Ctx.Input.RequestBody, &code)
if err != nil {
panic(err)
}
c.Data["json"] = object.UpdateCode(id, &code)
c.ServeJSON()
}
func (c *ApiController) AddCode() {
var code object.Code
err := json.Unmarshal(c.Ctx.Input.RequestBody, &code)
if err != nil {
panic(err)
}
c.Data["json"] = object.AddCode(&code)
c.ServeJSON()
}
func (c *ApiController) DeleteCode() {
var code object.Code
err := json.Unmarshal(c.Ctx.Input.RequestBody, &code)
if err != nil {
panic(err)
}
c.Data["json"] = object.DeleteCode(&code)
c.ServeJSON()
}

75
controllers/conference.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/confita/object"
)
func (c *ApiController) GetGlobalConferences() {
c.Data["json"] = object.GetGlobalConferences()
c.ServeJSON()
}
func (c *ApiController) GetConferences() {
owner := c.Input().Get("owner")
c.Data["json"] = object.GetConferences(owner)
c.ServeJSON()
}
func (c *ApiController) GetConference() {
id := c.Input().Get("id")
c.Data["json"] = object.GetConference(id)
c.ServeJSON()
}
func (c *ApiController) UpdateConference() {
id := c.Input().Get("id")
var conference object.Conference
err := json.Unmarshal(c.Ctx.Input.RequestBody, &conference)
if err != nil {
panic(err)
}
c.Data["json"] = object.UpdateConference(id, &conference)
c.ServeJSON()
}
func (c *ApiController) AddConference() {
var conference object.Conference
err := json.Unmarshal(c.Ctx.Input.RequestBody, &conference)
if err != nil {
panic(err)
}
c.Data["json"] = object.AddConference(&conference)
c.ServeJSON()
}
func (c *ApiController) DeleteConference() {
var conference object.Conference
err := json.Unmarshal(c.Ctx.Input.RequestBody, &conference)
if err != nil {
panic(err)
}
c.Data["json"] = object.DeleteConference(&conference)
c.ServeJSON()
}

39
controllers/payment.go Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casbin/confita/service"
func (c *ApiController) GetGlobalPayments() {
payments, err := service.GetGlobalPayments()
if err != nil {
panic(err)
}
c.Data["json"] = payments
c.ServeJSON()
}
func (c *ApiController) GetPayments() {
owner := c.Input().Get("owner")
payments, err := service.GetPayments(owner)
if err != nil {
panic(err)
}
c.Data["json"] = payments
c.ServeJSON()
}

27
controllers/product.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casbin/confita/service"
func (c *ApiController) GetProducts() {
products, err := service.GetProducts()
if err != nil {
panic(err)
}
c.Data["json"] = products
c.ServeJSON()
}

144
controllers/room.go Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/confita/object"
"github.com/casdoor/casdoor-go-sdk/auth"
)
func (c *ApiController) GetGlobalRooms() {
isPublic := c.Input().Get("isPublic")
if isPublic != "1" {
if c.RequireSignedIn() {
return
}
}
rooms := object.GetGlobalRooms()
if isPublic == "1" {
rooms = object.GetPublicRooms(rooms)
}
user := c.GetSessionUser()
if isPublic == "1" && (user == nil || !user.IsAdmin) {
if user == nil {
user = &auth.User{Name: ""}
}
rooms = object.GetMaskedRooms(rooms, user.Name)
}
rooms = object.GetRoomsWithLive(rooms)
c.Data["json"] = rooms
c.ServeJSON()
}
func (c *ApiController) GetRooms() {
if c.RequireSignedIn() {
return
}
owner := c.Input().Get("owner")
rooms := object.GetRooms(owner)
user := c.GetSessionUser()
if !user.IsAdmin {
rooms = object.GetMaskedRooms(rooms, user.Name)
}
rooms = object.GetRoomsWithLive(rooms)
c.Data["json"] = rooms
c.ServeJSON()
}
func (c *ApiController) GetRoom() {
//if c.RequireSignedIn() {
// return
//}
id := c.Input().Get("id")
room := object.GetRoom(id)
user := c.GetSessionUser()
if user == nil {
user = &auth.User{Name: ""}
}
if !user.IsAdmin {
room = object.GetMaskedRoom(room, user.Name)
}
room = object.GetRoomWithLive(room)
c.Data["json"] = room
c.ServeJSON()
}
func (c *ApiController) UpdateRoom() {
id := c.Input().Get("id")
var room object.Room
err := json.Unmarshal(c.Ctx.Input.RequestBody, &room)
if err != nil {
panic(err)
}
c.Data["json"] = object.UpdateRoom(id, &room)
c.ServeJSON()
}
func (c *ApiController) IncrementRoomViewer() {
id := c.Input().Get("id")
c.Data["json"] = object.IncrementRoomViewer(id)
c.ServeJSON()
}
func (c *ApiController) AddRoom() {
var room object.Room
err := json.Unmarshal(c.Ctx.Input.RequestBody, &room)
if err != nil {
panic(err)
}
c.Data["json"] = object.AddRoom(&room)
c.ServeJSON()
}
func (c *ApiController) DeleteRoom() {
var room object.Room
err := json.Unmarshal(c.Ctx.Input.RequestBody, &room)
if err != nil {
panic(err)
}
c.Data["json"] = object.DeleteRoom(&room)
c.ServeJSON()
}
func (c *ApiController) RegisterRoom() {
if c.RequireSignedIn() {
return
}
id := c.Input().Get("id")
username := c.GetSessionUsername()
c.Data["json"] = object.RegisterRoom(id, username)
c.ServeJSON()
}

View File

@ -0,0 +1,73 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"time"
"github.com/casbin/confita/object"
)
type MeetingEvent struct {
Event string `json:"event"`
Payload struct {
AccountId string `json:"account_id"`
Object struct {
Duration int `json:"duration"`
StartTime time.Time `json:"start_time"`
Timezone string `json:"timezone"`
EndTime time.Time `json:"end_time"`
Topic string `json:"topic"`
Id string `json:"id"`
Type int `json:"type"`
Uuid string `json:"uuid"`
HostId string `json:"host_id"`
} `json:"object"`
} `json:"payload"`
EventTs int64 `json:"event_ts"`
}
var messageMap map[string]int
func init() {
messageMap = map[string]int{}
}
func (c *ApiController) WebhookRoom() {
// https://marketplace.zoom.us/docs/guides/build/webhook-only-app/
var event MeetingEvent
err := json.Unmarshal(c.Ctx.Input.RequestBody, &event)
if err != nil {
panic(err)
}
status := ""
if event.Event == "webinar.started" {
status = "Started"
} else if event.Event == "webinar.ended" {
status = "Ended"
} else {
c.Data["json"] = true
c.ServeJSON()
return
}
meetingNumber := event.Payload.Object.Id
object.UpdateRoomStatus(meetingNumber, status)
c.Data["json"] = true
c.ServeJSON()
}

45
controllers/session.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"github.com/casdoor/casdoor-go-sdk/auth"
)
var sessionMap = map[string]int64{}
func clearUserDuplicated(claims *auth.Claims) {
userId := fmt.Sprintf("%s/%s", claims.Owner, claims.Name)
delete(sessionMap, userId)
}
func isUserDuplicated(claims *auth.Claims) bool {
userId := fmt.Sprintf("%s/%s", claims.Owner, claims.Name)
unixTimestamp := claims.IssuedAt.Unix()
sessionUnixTimestamp, ok := sessionMap[userId]
if !ok {
sessionMap[userId] = unixTimestamp
return false
} else {
if unixTimestamp == sessionUnixTimestamp {
return false
} else {
return true
}
}
}

75
controllers/submission.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/confita/object"
)
func (c *ApiController) GetGlobalSubmissions() {
c.Data["json"] = object.GetGlobalSubmissions()
c.ServeJSON()
}
func (c *ApiController) GetSubmissions() {
owner := c.Input().Get("owner")
c.Data["json"] = object.GetSubmissions(owner)
c.ServeJSON()
}
func (c *ApiController) GetSubmission() {
id := c.Input().Get("id")
c.Data["json"] = object.GetSubmission(id)
c.ServeJSON()
}
func (c *ApiController) UpdateSubmission() {
id := c.Input().Get("id")
var submission object.Submission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &submission)
if err != nil {
panic(err)
}
c.Data["json"] = object.UpdateSubmission(id, &submission)
c.ServeJSON()
}
func (c *ApiController) AddSubmission() {
var submission object.Submission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &submission)
if err != nil {
panic(err)
}
c.Data["json"] = object.AddSubmission(&submission)
c.ServeJSON()
}
func (c *ApiController) DeleteSubmission() {
var submission object.Submission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &submission)
if err != nil {
panic(err)
}
c.Data["json"] = object.DeleteSubmission(&submission)
c.ServeJSON()
}

View File

@ -0,0 +1,55 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/url"
"github.com/casbin/confita/service"
)
func getFileBytes(file *multipart.File) []byte {
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, *file); err != nil {
panic(err)
}
return buf.Bytes()
}
func (c *ApiController) UploadSubmissionFile() {
var resp Response
owner := c.GetSessionUsername()
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
panic(err)
}
filename := header.Filename
filename = url.QueryEscape(filename)
fileBytes := getFileBytes(&file)
fileUrl, objectKey := service.UploadFileToStorage(owner, "file", "UploadSubmissionFile", fmt.Sprintf("confita/file/%s/%s/%s", owner, "submissions", filename), fileBytes)
resp = Response{Status: "ok", Msg: fileUrl, Data: len(fileBytes), Data2: objectKey}
c.Data["json"] = resp
c.ServeJSON()
}

View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE+TCCAuGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMDYxHTAbBgNVBAoTFENh
c2Rvb3IgT3JnYW5pemF0aW9uMRUwEwYDVQQDEwxDYXNkb29yIENlcnQwHhcNMjEx
MDE1MDgxMTUyWhcNNDExMDE1MDgxMTUyWjA2MR0wGwYDVQQKExRDYXNkb29yIE9y
Z2FuaXphdGlvbjEVMBMGA1UEAxMMQ2FzZG9vciBDZXJ0MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAsInpb5E1/ym0f1RfSDSSE8IR7y+lw+RJjI74e5ej
rq4b8zMYk7HeHCyZr/hmNEwEVXnhXu1P0mBeQ5ypp/QGo8vgEmjAETNmzkI1NjOQ
CjCYwUrasO/f/MnI1C0j13vx6mV1kHZjSrKsMhYY1vaxTEP3+VB8Hjg3MHFWrb07
uvFMCJe5W8+0rKErZCKTR8+9VB3janeBz//zQePFVh79bFZate/hLirPK0Go9P1g
OvwIoC1A3sarHTP4Qm/LQRt0rHqZFybdySpyWAQvhNaDFE7mTstRSBb/wUjNCUBD
PTSLVjC04WllSf6Nkfx0Z7KvmbPstSj+btvcqsvRAGtvdsB9h62Kptjs1Yn7GAuo
I3qt/4zoKbiURYxkQJXIvwCQsEftUuk5ew5zuPSlDRLoLByQTLbx0JqLAFNfW3g/
pzSDjgd/60d6HTmvbZni4SmjdyFhXCDb1Kn7N+xTojnfaNkwep2REV+RMc0fx4Gu
hRsnLsmkmUDeyIZ9aBL9oj11YEQfM2JZEq+RVtUx+wB4y8K/tD1bcY+IfnG5rBpw
IDpS262boq4SRSvb3Z7bB0w4ZxvOfJ/1VLoRftjPbLIf0bhfr/AeZMHpIKOXvfz4
yE+hqzi68wdF0VR9xYc/RbSAf7323OsjYnjjEgInUtRohnRgCpjIk/Mt2Kt84Kb0
wn8CAwEAAaMQMA4wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAn2lf
DKkLX+F1vKRO/5gJ+Plr8P5NKuQkmwH97b8CS2gS1phDyNgIc4/LSdzuf4Awe6ve
C06lVdWSIis8UPUPdjmT2uMPSNjwLxG3QsrimMURNwFlLTfRem/heJe0Zgur9J1M
8haawdSdJjH2RgmFoDeE2r8NVRfhbR8KnCO1ddTJKuS1N0/irHz21W4jt4rxzCvl
2nR42Fybap3O/g2JXMhNNROwZmNjgpsF7XVENCSuFO1jTywLaqjuXCg54IL7XVLG
omKNNNcc8h1FCeKj/nnbGMhodnFWKDTsJcbNmcOPNHo6ixzqMy/Hqc+mWYv7maAG
Jtevs3qgMZ8F9Qzr3HpUc6R3ZYYWDY/xxPisuKftOPZgtH979XC4mdf0WPnOBLqL
2DJ1zaBmjiGJolvb7XNVKcUfDXYw85ZTZQ5b9clI4e+6bmyWqQItlwt+Ati/uFEV
XzCj70B4lALX6xau1kLEpV9O1GERizYRz5P9NJNA7KoO5AVMp9w0DQTkt+LbXnZE
HHnWKy8xHQKZF9sR7YBPGLs/Ac6tviv5Ua15OgJ/8dLRZ/veyFfGo2yZsI+hKVU5
nCCJHBcAyFnm1hdvdwEdH33jDBjNB6ciotJZrf/3VYaIWSalADosHAgMWfXuWP+h
8XKXmzlxuHbTMQYtZPDgspS5aK+S4Q9wb8RRAYo=
-----END CERTIFICATE-----

22
controllers/user.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casbin/confita/casdoor"
func (c *ApiController) GetUsers() {
c.Data["json"] = casdoor.GetUsers()
c.ServeJSON()
}

68
controllers/util.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
}
func (c *ApiController) ResponseOk(data ...interface{}) {
resp := Response{Status: "ok"}
switch len(data) {
case 2:
resp.Data2 = data[1]
fallthrough
case 1:
resp.Data = data[0]
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) ResponseError(error string, data ...interface{}) {
resp := Response{Status: "error", Msg: error}
switch len(data) {
case 2:
resp.Data2 = data[1]
fallthrough
case 1:
resp.Data = data[0]
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) RequireSignedIn() bool {
if c.GetSessionUser() == nil {
c.ResponseError("please sign in first")
return true
}
return false
}
func (c *ApiController) RequireAdmin() bool {
user := c.GetSessionUser()
if user == nil || !user.IsAdmin {
c.ResponseError("this operation requires admin privilege")
return true
}
return false
}

30
go.mod Normal file
View File

@ -0,0 +1,30 @@
module github.com/casbin/confita
go 1.16
require (
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1790
github.com/aliyun/aliyun-oss-go-sdk v2.1.9+incompatible
github.com/astaxie/beego v1.12.3
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/casdoor/casdoor-go-sdk v0.3.3
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/chromedp v0.8.6
github.com/cristalhq/jwt/v4 v4.0.0
github.com/go-sql-driver/mysql v1.5.0
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.3.0
github.com/nomeguy/zoom-go v0.0.6
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/tools v0.1.5 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
xorm.io/core v0.7.3
xorm.io/xorm v1.1.2
)

689
go.sum Normal file
View File

@ -0,0 +1,689 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1790 h1:PRZ7Qund25h+MxuC0WLeX6pdMkag5fb+rLrXe856LZo=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1790/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/aliyun-oss-go-sdk v2.1.9+incompatible h1:mO8fA9l5cQ7r0D2v3WribTT1GGbNVtnVviKM51jH6lI=
github.com/aliyun/aliyun-oss-go-sdk v2.1.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
github.com/casdoor/casdoor-go-sdk v0.3.3 h1:oyUNjpkC6zOEwBtY96qzqIQliQLsHZguOdrwItcGWnY=
github.com/casdoor/casdoor-go-sdk v0.3.3/go.mod h1:MBed3ISHQfXTtoOCAk5T8l5lt4wFvsyynrw0awggydY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777 h1:nEnjcdmVQjhtQm0RFJxRINMw7lsQ8gidtbpsidiDqpY=
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.6 h1:KobeeqR2dpfKSG1prS3Y6+FbffMmGC6xmAobRXA9QEQ=
github.com/chromedp/chromedp v0.8.6/go.mod h1:nBYHoD6YSNzrr82cIeuOzhw1Jo/s2o0QQ+ifTeoCZ+c=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
github.com/cristalhq/jwt/v4 v4.0.0 h1:gy4qHXjr7U8aPn9G8onlLfnC2UpjTyOg9QvEK/sDqq4=
github.com/cristalhq/jwt/v4 v4.0.0/go.mod h1:HnYraSNKDRag1DZP92rYHyrjyQHnVEHPNqesmzs+miQ=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
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=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nomeguy/zoom-go v0.0.6 h1:ryr5hY3dSHY5e+mAGSZQDQeX62FZrxinJ81OPqBxTjI=
github.com/nomeguy/zoom-go v0.0.6/go.mod h1:E1EuRYQp1u9xNUznZ3bwFCZSlDKRfY/JrPJ9S/CO23c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/cc/v3 v3.31.5-0.20210308123301-7a3e9dab9009 h1:u0oCo5b9wyLr++HF3AN9JicGhkUxJhMz51+8TIZH9N0=
modernc.org/cc/v3 v3.31.5-0.20210308123301-7a3e9dab9009/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
modernc.org/ccgo/v3 v3.9.0 h1:JbcEIqjw4Agf+0g3Tc85YvfYqkkFOv6xBwS4zkfqSoA=
modernc.org/ccgo/v3 v3.9.0/go.mod h1:nQbgkn8mwzPdp4mm6BT6+p85ugQ7FrGgIcYaE7nSrpY=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.8.0 h1:Pp4uv9g0csgBMpGPABKtkieF6O5MGhfGo6ZiOdlYfR8=
modernc.org/libc v1.8.0/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.10.1-0.20210314190707-798bbeb9bb84 h1:rgEUzE849tFlHSoeCrKyS9cZAljC+DY7MdMHKq6R6sY=
modernc.org/sqlite v1.10.1-0.20210314190707-798bbeb9bb84/go.mod h1:PGzq6qlhyYjL6uVbSgS6WoF7ZopTW/sI7+7p+mb4ZVU=
modernc.org/strutil v1.1.0 h1:+1/yCzZxY2pZwwrsbH+4T7BQMoLQ9QiBshRC9eicYsc=
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/tcl v1.5.0/go.mod h1:gb57hj4pO8fRrK54zveIfFXBaMHK3SKJNWcmRw1cRzc=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
xorm.io/builder v0.3.8 h1:P/wPgRqa9kX5uE0aA1/ukJ23u9KH0aSRpHLwDKXigSE=
xorm.io/builder v0.3.8/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0=
xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/xorm v1.1.2 h1:bje+1KZvK3m5AHtZNfUDlKEEyuw/IRHT+an0CLIG5TU=
xorm.io/xorm v1.1.2/go.mod h1:Cb0DKYTHbyECMaSfgRnIZp5aiUgQozxcJJ0vzcLGJSg=

97
i18n/generate.go Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/casbin/confita/util"
)
type I18nData map[string]map[string]string
var reI18n *regexp.Regexp
func init() {
reI18n, _ = regexp.Compile("i18next.t\\(\"(.*)\"\\)")
}
func getAllI18nStrings(fileContent string) []string {
res := []string{}
matches := reI18n.FindAllStringSubmatch(fileContent, -1)
if matches == nil {
return res
}
for _, match := range matches {
res = append(res, match[1])
}
return res
}
func getAllJsFilePaths() []string {
path := "../web/src"
res := []string{}
err := filepath.Walk(path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !strings.HasSuffix(info.Name(), ".js") {
return nil
}
res = append(res, path)
fmt.Println(path, info.Name())
return nil
})
if err != nil {
panic(err)
}
return res
}
func parseToData() *I18nData {
allWords := []string{}
paths := getAllJsFilePaths()
for _, path := range paths {
fileContent := util.ReadStringFromPath(path)
words := getAllI18nStrings(fileContent)
allWords = append(allWords, words...)
}
fmt.Printf("%v\n", allWords)
data := I18nData{}
for _, word := range allWords {
tokens := strings.Split(word, ":")
namespace := tokens[0]
key := tokens[1]
if _, ok := data[namespace]; !ok {
data[namespace] = map[string]string{}
}
data[namespace][key] = key
}
return &data
}

28
i18n/generate_test.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import "testing"
func TestGenerateI18nStrings(t *testing.T) {
dataEn := parseToData()
writeI18nFile("en", dataEn)
dataZh := readI18nFile("zh")
println(dataZh)
applyData(dataEn, dataZh)
writeI18nFile("zh", dataEn)
}

61
i18n/util.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import (
"fmt"
"github.com/casbin/confita/util"
)
func getI18nFilePath(language string) string {
return fmt.Sprintf("../web/src/locales/%s/data.json", language)
}
func readI18nFile(language string) *I18nData {
s := util.ReadStringFromPath(getI18nFilePath(language))
data := &I18nData{}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
return data
}
func writeI18nFile(language string, data *I18nData) {
s := util.StructToJson(data)
println(s)
util.WriteStringToPath(s, getI18nFilePath(language))
}
func applyData(data1 *I18nData, data2 *I18nData) {
for namespace, pairs2 := range *data2 {
if _, ok := (*data1)[namespace]; !ok {
continue
}
pairs1 := (*data1)[namespace]
for key, value := range pairs2 {
if _, ok := pairs1[key]; !ok {
continue
}
pairs1[key] = value
}
}
}

68
main.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/astaxie/beego/plugins/cors"
_ "github.com/astaxie/beego/session/redis"
"github.com/casbin/confita/casdoor"
"github.com/casbin/confita/conf"
"github.com/casbin/confita/object"
"github.com/casbin/confita/proxy"
"github.com/casbin/confita/routers"
_ "github.com/casbin/confita/routers"
)
func main() {
object.InitAdapter()
casdoor.InitCasdoorAdapter()
proxy.InitHttpClient()
object.InitRoomClient()
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"},
AllowHeaders: []string{"Origin", "X-Requested-With", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
// Cors Post method issue
// https://github.com/astaxie/beego/issues/1037
beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) {
if ctx.Input.Method() == "OPTIONS" {
ctx.WriteString("ok")
}
})
//beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static")
// https://studygolang.com/articles/2303
beego.InsertFilter("*", beego.BeforeRouter, routers.BotFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.Static)
if conf.GetConfigString("redisEndpoint") == "" {
beego.BConfig.WebConfig.Session.SessionProvider = "file"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"
} else {
beego.BConfig.WebConfig.Session.SessionProvider = "redis"
beego.BConfig.WebConfig.Session.SessionProviderConfig = conf.GetConfigString("redisEndpoint")
}
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 365
beego.Run()
}

121
object/adapter.go Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"runtime"
"github.com/astaxie/beego"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
var adapter *Adapter
func InitConfig() {
err := beego.LoadAppConfig("ini", "../conf/app.conf")
if err != nil {
panic(err)
}
InitAdapter()
}
func InitAdapter() {
adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"))
}
// Adapter represents the MySQL adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
engine *xorm.Engine
}
// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
err := a.engine.Close()
if err != nil {
panic(err)
}
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(driverName string, dataSourceName string) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
// Open the DB, create it if not existed.
a.open()
// Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer)
return a
}
func (a *Adapter) createDatabase() error {
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName)
if err != nil {
return err
}
defer engine.Close()
_, err = engine.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s default charset utf8 COLLATE utf8_general_ci", beego.AppConfig.String("dbName")))
return err
}
func (a *Adapter) open() {
if err := a.createDatabase(); err != nil {
panic(err)
}
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+beego.AppConfig.String("dbName"))
if err != nil {
panic(err)
}
a.engine = engine
a.createTable()
}
func (a *Adapter) close() {
a.engine.Close()
a.engine = nil
}
func (a *Adapter) createTable() {
err := a.engine.Sync2(new(Conference))
if err != nil {
panic(err)
}
err = a.engine.Sync2(new(Code))
if err != nil {
panic(err)
}
err = a.engine.Sync2(new(Submission))
if err != nil {
panic(err)
}
err = a.engine.Sync2(new(Room))
if err != nil {
panic(err)
}
}

102
object/code.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casbin/confita/util"
"xorm.io/core"
)
type Code struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Notebook string `xorm:"varchar(100)" json:"notebook"`
Tags []string `xorm:"varchar(100)" json:"tags"`
ImgUrl string `xorm:"varchar(100)" json:"imgUrl"`
}
func GetGlobalCodes() []*Code {
codes := []*Code{}
err := adapter.engine.Asc("owner").Desc("created_time").Find(&codes)
if err != nil {
panic(err)
}
return codes
}
func GetCodes(owner string) []*Code {
codes := []*Code{}
err := adapter.engine.Desc("created_time").Find(&codes, &Code{Owner: owner})
if err != nil {
panic(err)
}
return codes
}
func getCode(owner string, name string) *Code {
code := Code{Owner: owner, Name: name}
existed, err := adapter.engine.Get(&code)
if err != nil {
panic(err)
}
if existed {
return &code
} else {
return nil
}
}
func GetCode(id string) *Code {
owner, name := util.GetOwnerAndNameFromId(id)
return getCode(owner, name)
}
func UpdateCode(id string, code *Code) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCode(owner, name) == nil {
return false
}
_, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(code)
if err != nil {
panic(err)
}
//return affected != 0
return true
}
func AddCode(code *Code) bool {
affected, err := adapter.engine.Insert(code)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCode(code *Code) bool {
affected, err := adapter.engine.ID(core.PK{code.Owner, code.Name}).Delete(&Code{})
if err != nil {
panic(err)
}
return affected != 0
}

133
object/conference.go Normal file
View File

@ -0,0 +1,133 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casbin/confita/util"
"xorm.io/core"
)
type TreeItem struct {
Key string `xorm:"varchar(100)" json:"key"`
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"mediumtext" json:"content"`
TitleEn string `xorm:"varchar(100)" json:"titleEn"`
ContentEn string `xorm:"mediumtext" json:"contentEn"`
Children []*TreeItem `xorm:"varchar(1000)" json:"children"`
}
type Conference struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Type string `xorm:"varchar(100)" json:"type"`
Introduction string `xorm:"mediumtext" json:"introduction"`
StartDate string `xorm:"varchar(100)" json:"startDate"`
EndDate string `xorm:"varchar(100)" json:"endDate"`
Organizer string `xorm:"varchar(100)" json:"organizer"`
Carousels []string `xorm:"mediumtext" json:"carousels"`
CarouselHeight string `xorm:"varchar(100)" json:"carouselHeight"`
Tags []string `xorm:"mediumtext" json:"tags"`
DatasetUrl string `xorm:"varchar(100)" json:"datasetUrl"`
DatasetPreviewUrl string `xorm:"varchar(100)" json:"datasetPreviewUrl"`
PreviewData string `xorm:"mediumtext" json:"previewData"`
ResultUrl string `xorm:"varchar(100)" json:"resultUrl"`
Bonus int `json:"bonus"`
PersonCount int `json:"personCount"`
DisplayState string `xorm:"varchar(100)" json:"displayState"`
Status string `xorm:"varchar(100)" json:"status"`
Language string `xorm:"varchar(100)" json:"language"`
Location string `xorm:"varchar(100)" json:"location"`
Address string `xorm:"varchar(100)" json:"address"`
EnableSubmission bool `json:"enableSubmission"`
DefaultItem string `xorm:"mediumtext" json:"defaultItem"`
TreeItems []*TreeItem `xorm:"mediumtext" json:"treeItems"`
}
func GetGlobalConferences() []*Conference {
conferences := []*Conference{}
err := adapter.engine.Asc("owner").Desc("created_time").Find(&conferences)
if err != nil {
panic(err)
}
return conferences
}
func GetConferences(owner string) []*Conference {
conferences := []*Conference{}
err := adapter.engine.Desc("created_time").Find(&conferences, &Conference{Owner: owner})
if err != nil {
panic(err)
}
return conferences
}
func getConference(owner string, name string) *Conference {
conference := Conference{Owner: owner, Name: name}
existed, err := adapter.engine.Get(&conference)
if err != nil {
panic(err)
}
if existed {
return &conference
} else {
return nil
}
}
func GetConference(id string) *Conference {
owner, name := util.GetOwnerAndNameFromId(id)
return getConference(owner, name)
}
func UpdateConference(id string, conference *Conference) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getConference(owner, name) == nil {
return false
}
_, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(conference)
if err != nil {
panic(err)
}
//return affected != 0
return true
}
func AddConference(conference *Conference) bool {
affected, err := adapter.engine.Insert(conference)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteConference(conference *Conference) bool {
affected, err := adapter.engine.ID(core.PK{conference.Owner, conference.Name}).Delete(&Conference{})
if err != nil {
panic(err)
}
return affected != 0
}

155
object/live.go Normal file
View File

@ -0,0 +1,155 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strings"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
"github.com/aliyun/alibaba-cloud-sdk-go/services/live"
)
var LiveClient *live.Client
func init() {
LiveClient = getLiveClient()
}
func getLiveClient() *live.Client {
config := sdk.NewConfig()
credential := credentials.NewAccessKeyCredential(clientId, clientSecret)
client, err := live.NewClientWithOptions("cn-beijing", config, credential)
if err != nil {
panic(err)
}
return client
}
func getLiveDomainOnlineCount(room *Room) map[string]int {
if room.StreamingDomain == "" {
return nil
}
request := live.CreateDescribeLiveDomainOnlineUserNumRequest()
request.Scheme = "https"
request.DomainName = room.StreamingDomain
response, err := LiveClient.DescribeLiveDomainOnlineUserNum(request)
if err != nil {
fmt.Println(err)
return nil
}
res := map[string]int{}
for _, streamInfo := range response.OnlineUserInfo.LiveStreamOnlineUserNumInfo {
tokens := strings.Split(streamInfo.StreamName, "/")
streamName := tokens[len(tokens)-1]
res[streamName] = 0
for _, subStreamInfo := range streamInfo.Infos.Info {
res[streamName] += int(subStreamInfo.UserNumber)
}
}
return res
}
func getLiveStreamOnlineMap(room *Room) map[string]int {
if room.IngestDomain == "" {
return nil
}
request := live.CreateDescribeLiveStreamsOnlineListRequest()
request.Scheme = "https"
request.DomainName = room.IngestDomain
request.AppName = room.Conference
request.StreamName = room.Name
response, err := LiveClient.DescribeLiveStreamsOnlineList(request)
if err != nil {
panic(err)
}
res := map[string]int{}
for _, info := range response.OnlineInfo.LiveStreamOnlineInfo {
res[info.StreamName] = 1
}
return res
}
func GetRoomWithLive(room *Room) *Room {
if room.IngestDomain == "" {
return room
}
domainOnlineCountMap := getLiveDomainOnlineCount(room)
streamOnlineMap := getLiveStreamOnlineMap(room)
_, isLive := streamOnlineMap[room.Name]
room.IsLive = isLive
if isLive {
if domainOnlineCountMap != nil {
room.LiveUserCount = domainOnlineCountMap[room.Name]
}
}
return room
}
func GetRoomsWithLive(rooms []*Room) []*Room {
if len(rooms) == 0 {
return rooms
}
isEnded := true
for _, room := range rooms {
if room.MeetingNumber != "" && room.MeetingNumber != "123456789" {
isEnded = false
}
}
if isEnded {
return rooms
}
var roomWithDomain *Room = nil
for _, room := range rooms {
if room.StreamingDomain != "" {
roomWithDomain = room
break
}
}
domainOnlineCountMap := getLiveDomainOnlineCount(roomWithDomain)
for _, room := range rooms {
if room.IngestDomain == "" {
continue
}
streamOnlineMap := getLiveStreamOnlineMap(room)
_, isLive := streamOnlineMap[room.Name]
room.IsLive = isLive
if isLive {
if domainOnlineCountMap != nil {
room.LiveUserCount = domainOnlineCountMap[room.Name]
}
}
}
return rooms
}

4
object/live_conf.go Normal file
View File

@ -0,0 +1,4 @@
package object
var clientId = "xxx"
var clientSecret = "xxx"

32
object/live_test.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"testing"
"time"
)
func TestGetLive(t *testing.T) {
InitConfig()
for {
room := getRoom("admin", "seminar_1")
fmt.Printf("%v, %v\n", getLiveStreamOnlineMap(room), getLiveDomainOnlineCount(room))
time.Sleep(time.Second)
}
}

338
object/room.go Normal file
View File

@ -0,0 +1,338 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strings"
"time"
"github.com/casbin/confita/casdoor"
"github.com/casbin/confita/util"
"xorm.io/core"
)
type Participant struct {
Name string `xorm:"varchar(100)" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Email string `xorm:"varchar(100)" json:"email"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Tag string `xorm:"varchar(100)" json:"tag"`
Role string `xorm:"varchar(100)" json:"role"`
JoinUrl string `xorm:"varchar(500)" json:"joinUrl"`
}
type Slot struct {
Type string `xorm:"varchar(100)" json:"type"`
Date string `xorm:"varchar(100)" json:"date"`
StartTime string `xorm:"varchar(100)" json:"startTime"`
EndTime string `xorm:"varchar(100)" json:"endTime"`
Title string `xorm:"varchar(100)" json:"title"`
Speaker string `xorm:"varchar(100)" json:"speaker"`
Location string `xorm:"varchar(100)" json:"location"`
VideoUrl string `xorm:"varchar(255)" json:"videoUrl"`
}
type Room struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Conference string `xorm:"varchar(100)" json:"conference"`
Speaker string `xorm:"varchar(100)" json:"speaker"`
Date string `xorm:"varchar(100)" json:"date"`
StartTime string `xorm:"varchar(100)" json:"startTime"`
EndTime string `xorm:"varchar(100)" json:"endTime"`
Location string `xorm:"varchar(100)" json:"location"`
ImageUrl string `xorm:"varchar(100)" json:"imageUrl"`
SdkKey string `xorm:"varchar(100)" json:"sdkKey"`
Signature string `xorm:"varchar(1000)" json:"signature"`
MeetingNumber string `xorm:"varchar(100)" json:"meetingNumber"`
Passcode string `xorm:"varchar(100)" json:"passcode"`
InviteLink string `xorm:"varchar(100)" json:"inviteLink"`
StartUrl string `xorm:"varchar(500)" json:"startUrl"`
Participants []*Participant `xorm:"mediumtext" json:"participants"`
Slots []*Slot `xorm:"mediumtext" json:"slots"`
Status string `xorm:"varchar(100)" json:"status"`
IsPublic bool `json:"isPublic"`
IngestDomain string `xorm:"varchar(100)" json:"ingestDomain"`
IngestAuthKey string `xorm:"varchar(100)" json:"ingestAuthKey"`
StreamingDomain string `xorm:"varchar(100)" json:"streamingDomain"`
StreamingAuthKey string `xorm:"varchar(100)" json:"streamingAuthKey"`
MobileStreamingAuthKey string `xorm:"varchar(100)" json:"mobileStreamingAuthKey"`
VideoWidth int `json:"videoWidth"`
VideoHeight int `json:"videoHeight"`
IsLive bool `json:"isLive"`
LiveUserCount int `json:"liveUserCount"`
ViewerCount int `json:"viewerCount"`
VideoUrl string `xorm:"varchar(255)" json:"videoUrl"`
}
func GetGlobalRooms() []*Room {
rooms := []*Room{}
err := adapter.engine.Desc("created_time").Find(&rooms, &Room{})
if err != nil {
panic(err)
}
for _, room := range rooms {
room.updateRoomStartUrl()
}
return rooms
}
func GetRooms(owner string) []*Room {
rooms := []*Room{}
err := adapter.engine.Desc("created_time").Find(&rooms, &Room{Owner: owner})
if err != nil {
panic(err)
}
for _, room := range rooms {
room.updateRoomStartUrl()
}
return rooms
}
func GetMaskedRoom(room *Room, username string) *Room {
if room == nil {
return nil
}
if room.SdkKey != "" {
room.SdkKey = "***"
}
if room.Signature != "" {
room.Signature = "***"
}
if room.MeetingNumber != "" {
room.MeetingNumber = "***"
}
if room.Passcode != "" {
room.Passcode = "***"
}
if room.InviteLink != "" {
room.InviteLink = "***"
}
if room.StartUrl != "" {
room.StartUrl = "***"
}
for _, participant := range room.Participants {
if participant.Name != username {
if participant.JoinUrl != "" {
participant.JoinUrl = "***"
}
}
}
return room
}
func GetMaskedRooms(rooms []*Room, username string) []*Room {
for _, room := range rooms {
room = GetMaskedRoom(room, username)
}
return rooms
}
func GetPublicRooms(rooms []*Room) []*Room {
res := []*Room{}
for _, room := range rooms {
if room.IsPublic {
res = append(res, room)
}
}
return res
}
func getRoom(owner string, name string) *Room {
room := Room{Owner: owner, Name: name}
existed, err := adapter.engine.Get(&room)
if err != nil {
panic(err)
}
if existed {
return &room
} else {
return nil
}
}
func GetRoom(id string) *Room {
owner, name := util.GetOwnerAndNameFromId(id)
room := getRoom(owner, name)
if room != nil && room.MeetingNumber != "" {
room.Signature = generateSignature(room.MeetingNumber, "1")
room.updateRoomStartUrl()
}
return room
}
func UpdateRoom(id string, room *Room) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getRoom(owner, name) == nil {
return false
}
if room.MeetingNumber != "" {
room.updateRoomRegistrants()
}
_, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(room)
if err != nil {
panic(err)
}
//return affected != 0
return true
}
func IncrementRoomViewer(id string) bool {
room := Room{}
owner, name := util.GetOwnerAndNameFromId(id)
_, err := adapter.engine.ID(core.PK{owner, name}).Incr("viewer_count").Update(room)
if err != nil {
panic(err)
}
//return affected != 0
return true
}
func (p *Room) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}
func AddRoom(room *Room) bool {
affected, err := adapter.engine.Insert(room)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteRoom(room *Room) bool {
affected, err := adapter.engine.ID(core.PK{room.Owner, room.Name}).Delete(&Room{})
if err != nil {
panic(err)
}
return affected != 0
}
func (room *Room) updateRoomRegistrants() {
for i, participant := range room.Participants {
if participant.JoinUrl == "" {
isPanelist := i < 99
joinUrl := addMeetingRegistrant(room.MeetingNumber, participant.Name, participant.DisplayName, participant.Email, participant.Affiliation, isPanelist)
participant.JoinUrl = joinUrl
}
}
}
func RegisterRoom(id string, username string) *Room {
room := GetRoom(id)
if room == nil {
return nil
}
for i, participant := range room.Participants {
if participant.Name == username {
if participant.JoinUrl == "" {
isPanelist := i < 99
joinUrl := addMeetingRegistrant(room.MeetingNumber, participant.Name, participant.DisplayName, participant.Email, participant.Affiliation, isPanelist)
participant.JoinUrl = joinUrl
UpdateRoom(room.GetId(), room)
return room
}
}
}
user := casdoor.GetUser(username)
participant := &Participant{
Name: username,
CreatedTime: util.GetCurrentTime(),
DisplayName: user.DisplayName,
Email: user.Email,
Affiliation: user.Affiliation,
Tag: user.Tag,
Role: "Panelist",
JoinUrl: "",
}
isPanelist := len(room.Participants) < 99
joinUrl := addMeetingRegistrant(room.MeetingNumber, participant.Name, participant.DisplayName, participant.Email, participant.Affiliation, isPanelist)
participant.JoinUrl = joinUrl
room.Participants = append(room.Participants, participant)
UpdateRoom(room.GetId(), room)
return room
}
func (room *Room) updateRoomStartUrl() {
if room.MeetingNumber == "" || room.MeetingNumber == "123456789" {
return
}
if room.StartUrl == "" {
startUrl := getMeetingStartUrl(room.MeetingNumber)
room.StartUrl = startUrl
UpdateRoom(room.GetId(), room)
return
}
tokens := strings.SplitN(room.StartUrl, "?zak=", 2)
if len(tokens) < 2 {
startUrl := getMeetingStartUrl(room.MeetingNumber)
room.StartUrl = startUrl
UpdateRoom(room.GetId(), room)
return
}
zakToken := tokens[1]
zakTokenExpireTime := getZakExpireTime(zakToken)
if zakTokenExpireTime.Before(time.Now()) {
startUrl := getMeetingStartUrl(room.MeetingNumber)
room.StartUrl = startUrl
UpdateRoom(room.GetId(), room)
}
}
func UpdateRoomStatus(meetingNumber string, status string) {
room := Room{Owner: "admin", MeetingNumber: meetingNumber}
_, err := adapter.engine.Get(&room)
if err != nil {
panic(err)
}
room.Status = status
UpdateRoom(room.GetId(), &room)
}

98
object/room_alg.go Normal file
View File

@ -0,0 +1,98 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"encoding/json"
"time"
"github.com/casbin/confita/util"
"github.com/cristalhq/jwt/v4"
)
type JwtPayload struct {
SdkKey string `json:"sdkKey"`
Mn string `json:"mn"`
Role string `json:"role"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
AppKey string `json:"appKey"`
TokenExp int64 `json:"tokenExp"`
}
type ZakClaims struct {
Aud string `json:"aud"`
Uid string `json:"uid"`
Iss string `json:"iss"`
Sk string `json:"sk"`
Sty int `json:"sty"`
Wcd string `json:"wcd"`
Clt int `json:"clt"`
Mnum string `json:"mnum"`
Exp int `json:"exp"`
Iat int `json:"iat"`
Aid string `json:"aid"`
Cid string `json:"cid"`
}
func generateJwtPayload(meetingNumber string, role string) string {
iat := time.Now().Unix() - 30
exp := iat + 60*60*2
payload := JwtPayload{
SdkKey: zoomSdkKey,
Mn: meetingNumber,
Role: role,
Iat: iat,
Exp: exp,
AppKey: zoomSdkKey,
TokenExp: exp,
}
res := util.StructToJsonCompact(payload)
return res
}
func generateSignature(meetingNumber string, role string) string {
examplePayload := generateJwtPayload(meetingNumber, role)
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(zoomSdkSecret))
if err != nil {
panic(err)
}
token, err := jwt.NewBuilder(signer).Build(examplePayload)
if err != nil {
panic(err)
}
return token.String()
}
func getZakExpireTime(token string) time.Time {
tokenBytes := []byte(token)
newToken, err := jwt.ParseNoVerify(tokenBytes)
if err != nil {
panic(err)
}
data := &ZakClaims{}
err = json.Unmarshal(newToken.Claims(), data)
if err != nil {
panic(err)
}
tm := time.Unix(int64(data.Exp), 0)
return tm
}

34
object/room_alg_test.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"testing"
"time"
)
func TestGenerateSignature(t *testing.T) {
println(generateSignature("123456789", "1"))
}
func TestAddMeetingRegistrant(t *testing.T) {
println(addMeetingRegistrant("123456789", "alice", "Alice", "alice@example.com", "Example Inc.", true))
}
func TestGetZakExpireTime(t *testing.T) {
token := ""
tm := getZakExpireTime(token)
println(tm.Format(time.RFC3339))
}

21
object/room_conf.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
var zoomSdkKey = ""
var zoomSdkSecret = ""
var zoomApiEndpoint = ""
var zoomJwtToken = ""

129
object/room_zoom.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casbin/confita/proxy"
"github.com/casbin/confita/util"
"github.com/nomeguy/zoom-go/zoomAPI"
)
var zoomClient zoomAPI.Client
func InitRoomClient() {
zoomClient = zoomAPI.NewClient(zoomApiEndpoint, zoomJwtToken, proxy.ProxyHttpClient)
zoomClient.IsWebinar = true
}
func getMeetingStartUrl(meetingNumber string) string {
meetingId := util.ParseInt(meetingNumber)
resp, err := zoomClient.GetMeeting(meetingId)
if err != nil {
panic(err)
}
return resp.StartUrl
}
func getMeetingRegistrantId(meetingNumber string, email string) string {
meetingId := util.ParseInt(meetingNumber)
resp, err := zoomClient.ListMeetingRegistrants(meetingId, "pending")
if err != nil {
panic(err)
}
for _, registrant := range resp.Registrants {
if email == registrant.Email {
return registrant.Id
}
}
return ""
}
func approveMeetingRegistrant(meetingNumber string, email string) {
id := getMeetingRegistrantId(meetingNumber, email)
if id == "" {
fmt.Printf("getMeetingRegistrantId() is empty, meetingNumber = %s, email = %s, may be it is panelist\n", meetingNumber, email)
return
}
meetingId := util.ParseInt(meetingNumber)
registrants := []zoomAPI.Registrant{{email, id}}
err := zoomClient.UpdateMeetingRegistrantStatus(meetingId, "approve", registrants)
if err != nil {
panic(err)
}
}
func addMeetingRegistrant(meetingNumber string, name string, displayName string, email string, affiliation string, isPanelist bool) string {
var resp zoomAPI.AddMeetingRegistrantResponse
var err error
email = fmt.Sprintf("%s%s@example-nowhere.com", util.GenerateId()[:8], util.GenerateId()[:8])
phone := fmt.Sprintf("186%s", util.GenerateNumber(10000000, 99999999))
if displayName == "" {
displayName = name
}
meetingId := util.ParseInt(meetingNumber)
resp, err = zoomClient.AddMeetingRegistrant(meetingId,
email,
displayName,
fmt.Sprintf("(%s)", name),
"",
"",
"",
"",
"",
phone,
"",
affiliation,
"",
"",
"",
"",
"",
nil)
if err != nil {
panic(err)
}
approveMeetingRegistrant(meetingNumber, email)
if isPanelist {
addWebinarPanelist(meetingNumber, displayName, email)
}
return resp.JoinUrl
}
func addWebinarPanelist(meetingNumber string, displayName string, email string) {
panelist := zoomAPI.Panelist{
Email: email,
Name: displayName,
}
panelists := []zoomAPI.Panelist{panelist}
meetingId := util.ParseInt(meetingNumber)
err := zoomClient.AddWebinarPanelists(meetingId, panelists)
if err != nil {
panic(err)
}
}

121
object/submission.go Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casbin/confita/util"
"xorm.io/core"
)
type AuthorItem struct {
Name string `xorm:"varchar(100)" json:"name"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Email string `xorm:"varchar(100)" json:"email"`
IsNotified bool `json:"isNotified"`
IsCorresponding bool `json:"isCorresponding"`
}
type Submission struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Conference string `xorm:"varchar(100)" json:"conference"`
Title string `xorm:"varchar(100)" json:"title"`
Authors []*AuthorItem `xorm:"varchar(2000)" json:"authors"`
Type string `xorm:"varchar(100)" json:"type"`
SubType string `xorm:"varchar(100)" json:"subType"`
AbsWordFileUrl string `xorm:"varchar(500)" json:"absWordFileUrl"`
AbsPdfFileUrl string `xorm:"varchar(500)" json:"absPdfFileUrl"`
FullWordFileUrl string `xorm:"varchar(500)" json:"fullWordFileUrl"`
FullPdfFileUrl string `xorm:"varchar(500)" json:"fullPdfFileUrl"`
FinalWordFileUrl string `xorm:"varchar(500)" json:"finalWordFileUrl"`
FinalPdfFileUrl string `xorm:"varchar(500)" json:"finalPdfFileUrl"`
Status string `xorm:"varchar(100)" json:"status"`
Code string `xorm:"mediumtext" json:"code"`
}
func GetGlobalSubmissions() []*Submission {
submissions := []*Submission{}
err := adapter.engine.Desc("created_time").Find(&submissions, &Submission{})
if err != nil {
panic(err)
}
return submissions
}
func GetSubmissions(owner string) []*Submission {
submissions := []*Submission{}
err := adapter.engine.Desc("created_time").Find(&submissions, &Submission{Owner: owner})
if err != nil {
panic(err)
}
return submissions
}
func getSubmission(owner string, name string) *Submission {
submission := Submission{Owner: owner, Name: name}
existed, err := adapter.engine.Get(&submission)
if err != nil {
panic(err)
}
if existed {
return &submission
} else {
return nil
}
}
func GetSubmission(id string) *Submission {
owner, name := util.GetOwnerAndNameFromId(id)
return getSubmission(owner, name)
}
func UpdateSubmission(id string, submission *Submission) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getSubmission(owner, name) == nil {
return false
}
_, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(submission)
if err != nil {
panic(err)
}
//return affected != 0
return true
}
func AddSubmission(submission *Submission) bool {
affected, err := adapter.engine.Insert(submission)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteSubmission(submission *Submission) bool {
affected, err := adapter.engine.ID(core.PK{submission.Owner, submission.Name}).Delete(&Submission{})
if err != nil {
panic(err)
}
return affected != 0
}

8
oss/conf.go Normal file
View File

@ -0,0 +1,8 @@
package oss
var endpoint = "xxx"
var accessKeyId = "xxx"
var accessKeySecret = "xxx"
var domain = "xxx"
var bucketName = "xxx"

40
oss/deploy.go Normal file
View File

@ -0,0 +1,40 @@
package oss
import (
"fmt"
"os"
"strings"
"github.com/casbin/confita/util"
)
func uploadFolder(folder string) {
path := fmt.Sprintf("../web/build/static/%s/", folder)
filenames := util.ListFiles(path)
bucket := getBucket()
for _, filename := range filenames {
file, err := os.Open(path + filename)
if err != nil {
panic(err)
}
objectKey := fmt.Sprintf("confita/static/%s/%s", folder, filename)
err = bucket.PutObject(objectKey, file)
if err != nil {
panic(err)
}
fmt.Printf("Uploaded [%s] to [%s]\n", path, objectKey)
}
}
func updateHtml() {
htmlPath := "../web/build/index.html"
html := util.ReadStringFromPath(htmlPath)
html = strings.Replace(html, "\"/static/", fmt.Sprintf("\"https://%s/confita/static/", domain), -1)
util.WriteStringToPath(html, htmlPath)
fmt.Printf("Updated HTML to [%s]\n", html)
}

10
oss/deploy_test.go Normal file
View File

@ -0,0 +1,10 @@
package oss
import "testing"
func TestDeploy(t *testing.T) {
uploadFolder("js")
uploadFolder("css")
updateHtml()
}

63
oss/oss.go Normal file
View File

@ -0,0 +1,63 @@
package oss
import (
"bytes"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
func getBucket() *oss.Bucket {
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
panic(err)
}
bucket, err := client.Bucket(bucketName)
if err != nil {
panic(err)
}
return bucket
}
func UploadFileAndGetLink(folder string, owner string, filename string, fileBytes []byte) (string, string) {
reader := bytes.NewReader(fileBytes)
bucket := getBucket()
objectKey := fmt.Sprintf("%s/%s/%s", folder, owner, filename)
err := bucket.PutObject(objectKey, reader)
if err != nil {
panic(err)
}
url := fmt.Sprintf("https://%s/%s/%s/%s", domain, folder, owner, filename)
return url, objectKey
}
func GetFile(folder string, owner string, filename string) (string, string) {
bucket := getBucket()
objectKey := fmt.Sprintf("%s/%s/%s", folder, owner, filename)
existed, err := bucket.IsObjectExist(objectKey)
if err != nil {
panic(err)
}
if existed {
url := fmt.Sprintf("https://%s/%s/%s/%s", domain, folder, owner, filename)
return url, objectKey
} else {
return "", objectKey
}
}
func DeleteFile(objectKey string) {
bucket := getBucket()
err := bucket.DeleteObject(objectKey)
if err != nil {
panic(err)
}
}

86
proxy/proxy.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/astaxie/beego"
"golang.org/x/net/proxy"
)
var (
DefaultHttpClient *http.Client
ProxyHttpClient *http.Client
)
func InitHttpClient() {
// not use proxy
DefaultHttpClient = http.DefaultClient
// use proxy
ProxyHttpClient = getProxyHttpClient()
}
func isAddressOpen(address string) bool {
timeout := time.Millisecond * 100
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
// cannot connect to address, proxy is not active
return false
}
if conn != nil {
defer conn.Close()
fmt.Printf("Socks5 proxy enabled: %s\n", address)
return true
}
return false
}
func getProxyHttpClient() *http.Client {
socks5Proxy := beego.AppConfig.String("socks5Proxy")
if socks5Proxy == "" {
return &http.Client{}
}
if !isAddressOpen(socks5Proxy) {
return &http.Client{}
}
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct)
if err != nil {
panic(err)
}
tr := &http.Transport{Dial: dialer.Dial}
return &http.Client{
Transport: tr,
}
}
func GetHttpClient(url string) *http.Client {
if strings.Contains(url, "githubusercontent.com") || strings.Contains(url, "googleusercontent.com") {
return ProxyHttpClient
} else {
return DefaultHttpClient
}
}

43
routers/filter.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
import (
"net/http"
"strings"
"github.com/astaxie/beego/context"
"github.com/casbin/confita/util"
)
func Static(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/api/") {
return
}
path := "web/build"
if urlPath == "/" {
path += "/index.html"
} else {
path += urlPath
}
if util.FileExist(path) {
http.ServeFile(ctx.ResponseWriter, ctx.Request, path)
} else {
http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html")
}
}

130
routers/filter_ssr.go Normal file
View File

@ -0,0 +1,130 @@
package routers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
)
// var chromeCtx ctx.Context
var chromeCtxPool *SsrPool
var (
isChromeInstalled bool
isChromeInit bool
)
type PageCache struct {
time time.Time
html string
}
var renderCache = make(map[string]PageCache)
// modified from https://github.com/chromedp/chromedp/blob/master/allocate.go#L331
func isChromeFound() bool {
for _, path := range [...]string{
// Unix-like
"headless_shell",
"headless-shell",
"chromium",
"chromium-browser",
"google-chrome",
"google-chrome-stable",
"google-chrome-beta",
"google-chrome-unstable",
"/usr/bin/google-chrome",
// Windows
"chrome",
"chrome.exe", // in case PATHEXT is misconfigured
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
`C:\Program Files\Google\Chrome\Application\chrome.exe`,
filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Google\Chrome\Application\chrome.exe`),
// Mac
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
} {
_, err := exec.LookPath(path)
if err == nil {
// return found -> return true
// modified by Kininaru<shiftregister233@outlook.com>
return true
}
}
// return "google-chrome" -> return false
// modified by Kininaru<shiftregister233@outlook.com>
return false
}
func InitChromeDp() {
isChromeInit = true
isChromeInstalled = isChromeFound()
if isChromeInstalled {
chromeCtxNum, _ := beego.AppConfig.Int("chromeCtxNum")
if chromeCtxNum <= 0 {
chromeCtxNum = 1 // default
}
chromeCtxPool = NewSsrPool(chromeCtxNum)
}
go chromeCtxPool.Run() // start ssr_pool
}
func cacheSave(urlString string, res string) {
renderCache[urlString] = PageCache{time.Now(), res}
}
func cacheRestore(urlString string, cacheExpireSeconds int64) (string, bool) {
if _, ok := renderCache[urlString]; ok {
if time.Now().Sub(renderCache[urlString].time) < time.Duration(cacheExpireSeconds)*time.Second {
return renderCache[urlString].html, true
}
}
return "", false
}
var botRegex *regexp.Regexp
func isBot(userAgent string) bool {
if botRegex == nil {
botRegex, _ = regexp.Compile("bot|slurp|bing|crawler|spider")
}
userAgent = strings.ToLower(userAgent)
return botRegex.MatchString(userAgent)
}
func BotFilter(ctx *context.Context) {
if strings.HasPrefix(ctx.Request.URL.Path, "/api/") {
return
}
if isBot(ctx.Request.UserAgent()) {
ctx.ResponseWriter.WriteHeader(200)
urlStr := fmt.Sprintf("http://%s%s", ctx.Request.Host, ctx.Request.URL.Path)
if !isChromeInit {
InitChromeDp()
}
if !isChromeInstalled {
_, err := ctx.ResponseWriter.Write([]byte("Chrome is not installed in your server"))
if err != nil {
panic(err)
}
}
// the context will be canceled when the task send to channel
// sync.WaitGroup will wait for the task to be finished, it can avoid this problem
var wg sync.WaitGroup
wg.Add(1)
// create ssr_task and put it into task channel
task := NewRenderTask(ctx, urlStr, &wg)
chromeCtxPool.TaskChannel <- task
wg.Wait()
}
}

77
routers/router.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
import (
"github.com/astaxie/beego"
"github.com/casbin/confita/controllers"
)
func init() {
initAPI()
}
func initAPI() {
ns :=
beego.NewNamespace("/api",
beego.NSInclude(
&controllers.ApiController{},
),
)
beego.AddNamespace(ns)
beego.Router("/api/signin", &controllers.ApiController{}, "POST:Signin")
beego.Router("/api/signout", &controllers.ApiController{}, "POST:Signout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/get-global-conferences", &controllers.ApiController{}, "GET:GetGlobalConferences")
beego.Router("/api/get-conferences", &controllers.ApiController{}, "GET:GetConferences")
beego.Router("/api/get-conference", &controllers.ApiController{}, "GET:GetConference")
beego.Router("/api/update-conference", &controllers.ApiController{}, "POST:UpdateConference")
beego.Router("/api/add-conference", &controllers.ApiController{}, "POST:AddConference")
beego.Router("/api/delete-conference", &controllers.ApiController{}, "POST:DeleteConference")
beego.Router("/api/get-global-codes", &controllers.ApiController{}, "GET:GetGlobalCodes")
beego.Router("/api/get-codes", &controllers.ApiController{}, "GET:GetCodes")
beego.Router("/api/get-code", &controllers.ApiController{}, "GET:GetCode")
beego.Router("/api/update-code", &controllers.ApiController{}, "POST:UpdateCode")
beego.Router("/api/add-code", &controllers.ApiController{}, "POST:AddCode")
beego.Router("/api/delete-code", &controllers.ApiController{}, "POST:DeleteCode")
beego.Router("/api/get-global-submissions", &controllers.ApiController{}, "GET:GetGlobalSubmissions")
beego.Router("/api/get-submissions", &controllers.ApiController{}, "GET:GetSubmissions")
beego.Router("/api/get-submission", &controllers.ApiController{}, "GET:GetSubmission")
beego.Router("/api/update-submission", &controllers.ApiController{}, "POST:UpdateSubmission")
beego.Router("/api/add-submission", &controllers.ApiController{}, "POST:AddSubmission")
beego.Router("/api/delete-submission", &controllers.ApiController{}, "POST:DeleteSubmission")
beego.Router("/api/upload-submission-file", &controllers.ApiController{}, "POST:UploadSubmissionFile")
beego.Router("/api/get-global-rooms", &controllers.ApiController{}, "GET:GetGlobalRooms")
beego.Router("/api/get-rooms", &controllers.ApiController{}, "GET:GetRooms")
beego.Router("/api/get-room", &controllers.ApiController{}, "GET:GetRoom")
beego.Router("/api/update-room", &controllers.ApiController{}, "POST:UpdateRoom")
beego.Router("/api/increment-room-viewer", &controllers.ApiController{}, "POST:IncrementRoomViewer")
beego.Router("/api/add-room", &controllers.ApiController{}, "POST:AddRoom")
beego.Router("/api/delete-room", &controllers.ApiController{}, "POST:DeleteRoom")
beego.Router("/api/register-room", &controllers.ApiController{}, "GET:RegisterRoom")
beego.Router("/api/webhook-room", &controllers.ApiController{}, "Post:WebhookRoom")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-global-payments", &controllers.ApiController{}, "GET:GetGlobalPayments")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
}

170
routers/ssr_pool.go Normal file
View File

@ -0,0 +1,170 @@
package routers
import (
ctx "context"
"errors"
"fmt"
"runtime"
"sync"
"time"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/astaxie/beego/logs"
"github.com/chromedp/chromedp"
)
var renderTimeout = 20 * time.Second
type RenderTask struct {
HttpCtx *context.Context
Url string
Render func(chromeCtx ctx.Context, url string) (string, error)
Wg *sync.WaitGroup
}
type SsrPool struct {
TaskChannel chan *RenderTask
JobsChannel chan *RenderTask
AddWorkerChannel chan bool
WorkerNum int
}
func NewRenderTask(httpCtx *context.Context, url string, wg *sync.WaitGroup) *RenderTask {
return &RenderTask{
HttpCtx: httpCtx,
Url: url,
Render: render,
Wg: wg,
}
}
func NewSsrPool(cap int) *SsrPool {
pool := SsrPool{
TaskChannel: make(chan *RenderTask),
JobsChannel: make(chan *RenderTask),
WorkerNum: cap,
}
return &pool
}
func render(chromeCtx ctx.Context, url string) (string, error) {
cacheExpireSeconds, err := beego.AppConfig.Int64("cacheExpireSeconds")
if err != nil {
return "", err
}
res, cacheHit := cacheRestore(url, cacheExpireSeconds)
if cacheHit {
return res, nil
}
// set timeout for render page
done := make(chan bool, 1)
go func() {
err = chromedp.Run(chromeCtx,
chromedp.Navigate(url),
chromedp.OuterHTML("html", &res),
)
if err != nil {
done <- false
} else {
done <- true
}
}()
select {
case success := <-done:
if success {
cacheSave(url, res)
return res, nil
} else {
return "", err
}
case <-time.After(renderTimeout):
err := chromedp.Cancel(chromeCtx)
if err != nil {
return "", errors.New("context cancel failed")
}
return "", ctx.Canceled
}
}
func (pool *SsrPool) worker() {
// chromeCtx, _ := chromedp.NewExecAllocator(ctx.Background(), append(
// chromedp.DefaultExecAllocatorOptions[:],
// chromedp.Flag("headless", false))...)
// chromeCtx, _ = chromedp.NewContext(chromeCtx)
chromeCtx, _ := chromedp.NewContext(ctx.Background()) // set default context with headless mode
for task := range pool.JobsChannel {
cancel := func() bool {
defer func() {
if err := recover(); err != nil {
handleErr(task.HttpCtx, err.(error))
task.Wg.Done()
}
}()
urlStr, err := task.Render(chromeCtx, task.Url)
if err != nil {
if err == ctx.Canceled { // when browser process has terminated
handleErr(task.HttpCtx, err)
task.Wg.Done()
return true
} else {
panic(err)
}
}
_, err = task.HttpCtx.ResponseWriter.Write([]byte(urlStr))
if err != nil {
panic(err)
}
task.Wg.Done()
return false
}()
// if canceled, break the loop
if cancel {
break
}
}
// if break, add a new worker
pool.AddWorkerChannel <- true
}
func (pool *SsrPool) Run() {
pool.AddWorkerChannel = make(chan bool, pool.WorkerNum)
for i := 0; i < pool.WorkerNum; i++ {
pool.AddWorkerChannel <- true
}
go func() {
for j := range pool.AddWorkerChannel {
if j == true {
go pool.worker()
}
}
}()
for task := range pool.TaskChannel {
pool.JobsChannel <- task
}
}
func handleErr(ctx *context.Context, err error) {
var stack string
logs.Critical("the request url is ", ctx.Input.URL())
logs.Critical("Handler crashed with error:", err)
for i := 1; ; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
logs.Critical(fmt.Sprintf("%s:%d", file, line))
stack = stack + fmt.Sprintln(fmt.Sprintf("%s:%d", file, line))
}
if ctx.Output.Status != 0 {
ctx.ResponseWriter.WriteHeader(ctx.Output.Status)
} else {
ctx.ResponseWriter.WriteHeader(500)
}
_, _err := ctx.ResponseWriter.Write([]byte(err.(error).Error()))
if _err != nil {
logs.Critical("write response error:", err)
}
}

35
service/oss.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import "github.com/casdoor/casdoor-go-sdk/auth"
func UploadFileToStorage(user string, tag string, parent string, fullFilePath string, fileBytes []byte) (string, string) {
fileUrl, objectKey, err := auth.UploadResource(user, tag, parent, fullFilePath, fileBytes)
if err != nil {
panic(err)
}
return fileUrl, objectKey
}
func DeleteFileFromStorage(objectKey string) bool {
affected, err := auth.DeleteResource(objectKey)
if err != nil {
panic(err)
}
return affected
}

109
service/payment.go Normal file
View File

@ -0,0 +1,109 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"encoding/json"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/auth"
)
type Payment struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Provider string `xorm:"varchar(100)" json:"provider"`
Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"`
ProductName string `xorm:"varchar(100)" json:"productName"`
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(1000)" json:"message"`
PersonName string `xorm:"varchar(100)" json:"personName"`
PersonIdCard string `xorm:"varchar(100)" json:"personIdCard"`
PersonEmail string `xorm:"varchar(100)" json:"personEmail"`
PersonPhone string `xorm:"varchar(100)" json:"personPhone"`
InvoiceType string `xorm:"varchar(100)" json:"invoiceType"`
InvoiceTitle string `xorm:"varchar(100)" json:"invoiceTitle"`
InvoiceTaxId string `xorm:"varchar(100)" json:"invoiceTaxId"`
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
}
func GetGlobalPayments() ([]*Payment, error) {
organization := beego.AppConfig.String("casdoorOrganization")
queryMap := map[string]string{}
url := auth.GetUrl("get-payments", queryMap)
bytes, err := auth.DoGetBytesRaw(url)
if err != nil {
return nil, err
}
var payments []*Payment
err = json.Unmarshal(bytes, &payments)
if err != nil {
return nil, err
}
res := []*Payment{}
for _, payment := range payments {
if payment.Organization == organization {
res = append(res, payment)
}
}
return res, nil
}
func GetPayments(user string) ([]*Payment, error) {
owner := "admin"
organization := beego.AppConfig.String("casdoorOrganization")
queryMap := map[string]string{
"owner": owner,
"organization": organization,
"user": user,
}
url := auth.GetUrl("get-user-payments", queryMap)
bytes, err := auth.DoGetBytes(url)
if err != nil {
return nil, err
}
var payments []*Payment
err = json.Unmarshal(bytes, &payments)
if err != nil {
return nil, err
}
return payments, nil
}

62
service/product.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"encoding/json"
"github.com/casdoor/casdoor-go-sdk/auth"
)
type Product struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetProducts() ([]*Product, error) {
owner := "admin"
queryMap := map[string]string{
"owner": owner,
}
url := auth.GetUrl("get-products", queryMap)
bytes, err := auth.DoGetBytesRaw(url)
if err != nil {
return nil, err
}
var products []*Product
err = json.Unmarshal(bytes, &products)
if err != nil {
return nil, err
}
return products, nil
}

39
util/json.go Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import "encoding/json"
func StructToJson(v interface{}) string {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
panic(err)
}
return string(data)
}
func StructToJsonCompact(v interface{}) string {
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
return string(data)
}
func JsonToStruct(data string, v interface{}) error {
return json.Unmarshal([]byte(data), v)
}

65
util/path.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"io/ioutil"
"os"
"path/filepath"
)
func FileExist(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func GetPath(path string) string {
return filepath.Dir(path)
}
func EnsureFileFolderExists(path string) {
p := GetPath(path)
if !FileExist(p) {
err := os.MkdirAll(p, os.ModePerm)
if err != nil {
panic(err)
}
}
}
func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))]
}
func ListFiles(path string) []string {
res := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, f := range files {
if !f.IsDir() {
res = append(res, f.Name())
}
}
return res
}

149
util/string.go Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"strconv"
"strings"
"unicode"
"github.com/google/uuid"
)
func IndexAt(s, sep string, n int) int {
idx := strings.Index(s[n:], sep)
if idx > -1 {
idx += n
}
return idx
}
func ParseInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func ParseIntWithError(s string) (int, error) {
i, err := strconv.Atoi(s)
if err != nil {
return -1, err
}
if i < 0 {
return -1, errors.New("negative version number")
}
return i, nil
}
func ParseFloat(s string) float64 {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
panic(err)
}
return f
}
func GetOwnerAndNameFromId(id string) (string, string) {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
panic(errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id))
}
return tokens[0], tokens[1]
}
func GetOwnerAndNameFromId3(id string) (string, string, string) {
tokens := strings.Split(id, "/")
if len(tokens) != 3 {
panic(errors.New("GetOwnerAndNameFromId3() error, wrong token count for ID: " + id))
}
return tokens[0], fmt.Sprintf("%s/%s", tokens[0], tokens[1]), tokens[2]
}
func GetOwnerAndNameFromId3New(id string) (string, string, string) {
tokens := strings.Split(id, "/")
if len(tokens) != 3 {
panic(errors.New("GetOwnerAndNameFromId3New() error, wrong token count for ID: " + id))
}
return tokens[0], tokens[1], tokens[2]
}
func GetIdFromOwnerAndName(owner string, name string) string {
return fmt.Sprintf("%s/%s", owner, name)
}
func ReadStringFromPath(path string) string {
data, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return string(data)
}
func WriteStringToPath(s string, path string) {
err := ioutil.WriteFile(path, []byte(s), 0644)
if err != nil {
panic(err)
}
}
func ReadBytesFromPath(path string) []byte {
data, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return data
}
func WriteBytesToPath(b []byte, path string) {
err := ioutil.WriteFile(path, b, 0644)
if err != nil {
panic(err)
}
}
func GenerateId() string {
return uuid.NewString()
}
func GenerateNumber(min int, max int) string {
res := rand.Intn(max-min) + min
return strconv.Itoa(res)
}
func IsChinese(str string) bool {
var flag bool
for _, v := range str {
if unicode.Is(unicode.Han, v) {
flag = true
break
}
}
return flag
}

23
util/time.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import "time"
func GetCurrentTime() string {
timestamp := time.Now().Unix()
tm := time.Unix(timestamp, 0)
return tm.Format(time.RFC3339)
}

117
web/.eslintrc Normal file
View File

@ -0,0 +1,117 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"requireConfigFile": false,
"babelOptions": {
"babelrc": false,
"configFile": false,
"presets": ["@babel/preset-react"]
}
},
"settings": {
"react": {
"version": "detect"
}
},
"plugins": ["unused-imports"],
"extends": ["eslint:recommended", "plugin:react/recommended"],
"rules": {
"semi": ["error", "always"],
"indent": ["error", 2],
// follow antd's style guide
"quotes": ["error", "double"],
"jsx-quotes": ["error", "prefer-double"],
"space-in-parens": ["error", "never"],
"object-curly-spacing": ["error", "never"],
"array-bracket-spacing": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"react/jsx-curly-spacing": [
"error",
{ "when": "never", "allowMultiline": true, "children": true }
],
"arrow-spacing": ["error", { "before": true, "after": true }],
"space-before-blocks": ["error", "always"],
"spaced-comment": ["error", "always"],
"react/jsx-tag-spacing": ["error", { "beforeSelfClosing": "always" }],
"block-spacing": ["error", "never"],
"space-before-function-paren": ["error", "never"],
"no-trailing-spaces": ["error", { "ignoreComments": true }],
"eol-last": ["error", "always"],
"no-var": ["error"],
"prefer-const": [
"error",
{
"destructuring": "all"
}
],
"curly": ["error", "all"],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"no-mixed-spaces-and-tabs": "error",
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true
}
],
"no-multiple-empty-lines": [
"error",
{ "max": 1, "maxBOF": 0, "maxEOF": 0 }
],
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
"space-infix-ops": "error",
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
"comma-style": ["error", "last"],
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "never",
"exports": "never",
"functions": "never"
}
],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "none",
"argsIgnorePattern": "^_"
}
],
"no-unused-vars": "off",
"react/no-deprecated": "error",
"react/jsx-key": "error",
"no-console": "error",
"eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off",
"react/display-name": "off",
"react/react-in-jsx-scope": "off",
"no-case-declarations": "off"
},
"overrides": [
{
"files": [
"**/*.test.js"
],
"env": {
"jest": true
}
}
]
}

23
web/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

70
web/README.md Normal file
View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

17
web/babel.config.json Normal file
View File

@ -0,0 +1,17 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

17
web/craco.config.js Normal file
View File

@ -0,0 +1,17 @@
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { "@primary-color": "#7f5de3", "@border-radius-base": "5px" },
javascriptEnabled: true,
},
},
},
},
],
};

67
web/package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "4.6.2",
"@craco/craco": "6.1.1",
"aliplayer-react": "^0.7.0",
"antd": "4.15.5",
"braft-editor": "^2.3.9",
"casdoor-js-sdk": "^0.0.3",
"codemirror": "^5.65.3",
"copy-to-clipboard": "^3.3.1",
"craco-less": "2.0.0",
"file-saver": "2.0.2",
"i18next": "^19.8.9",
"moment": "^2.29.1",
"qrcode.react": "^3.0.2",
"react": "^18.2.0",
"react-codemirror2": "^7.2.1",
"react-datasheet": "^1.4.9",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-hot-keys": "^2.7.2",
"react-i18next": "^11.8.7",
"react-papaparse": "3.18.2",
"react-router-dom": "^5.1.2",
"react-scripts": "5.0.1",
"xlsx": "0.16.9"
},
"scripts": {
"start": "set PORT=11001 && craco start",
"build": "del build.zip 2>nul && set \"GENERATE_SOURCEMAP=false\" && set \"SKIP_PREFLIGHT_CHECK=true\" && craco build && 360zip.exe -ar build %cd%/build.zip || ECHO.",
"test": "craco test",
"eject": "craco eject",
"analyze": "source-map-explorer 'build/static/js/*.js'",
"fix": "eslint --fix src/*.js src/backend/*.js",
"lint": "eslint src/*.js src/backend/*.js"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all",
"ie 9, ie 10, ie 11"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version",
"ie 9, ie 10, ie 11"
]
},
"devDependencies": {
"@babel/core": "^7.19.3",
"@babel/eslint-parser": "^7.19.1",
"eslint": "^8.25.0",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-unused-imports": "^2.0.0"
}
}

39
web/public/index.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="https://cdn.casbin.com/static/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Confita</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

25
web/public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "Confita",
"name": "Confita",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

2
web/src/.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

579
web/src/App.js Normal file
View File

@ -0,0 +1,579 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {Component} from "react";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import {Avatar, BackTop, Col, Dropdown, Layout, List, Menu, Modal, Popover, Row} from "antd";
import {CloseCircleTwoTone, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import "./App.less";
import * as Setting from "./Setting";
import * as AccountBackend from "./backend/AccountBackend";
import AuthCallback from "./AuthCallback";
import * as Conf from "./Conf";
import HomePage from "./HomePage";
import PaymentPage from "./PaymentPage";
import ContactPage from "./ContactPage";
import ConferenceListPage from "./ConferenceListPage";
import ConferenceEditPage from "./ConferenceEditPage";
import SubmissionListPage from "./SubmissionListPage";
import SubmissionEditPage from "./SubmissionEditPage";
import PaymentListPage from "./PaymentListPage";
import RoomListPage from "./RoomListPage";
import RoomEditPage from "./RoomEditPage";
import RoomPage from "./RoomPage";
import SigninPage from "./SigninPage";
import i18next from "i18next";
import SelectLanguageBox from "./SelectLanguageBox";
import CompetitionListPage from "./CompetitionListPage";
import CodeListPage from "./CodeListPage";
import CodeEditPage from "./CodeEditPage";
import {withTranslation} from "react-i18next";
const {Header, Footer} = Layout;
class App extends Component {
constructor(props) {
super(props);
this.state = {
classes: props,
selectedMenuKey: 0,
account: undefined,
isDupSession: false,
uri: null,
};
Setting.initServerUrl();
Setting.initCasdoorSdk(Conf.AuthConfig);
}
UNSAFE_componentWillMount() {
this.updateMenuKey();
this.getAccount();
}
componentDidUpdate() {
// eslint-disable-next-line no-restricted-globals
const uri = location.pathname;
if (this.state.uri !== uri) {
this.updateMenuKey();
}
}
updateMenuKey() {
// eslint-disable-next-line no-restricted-globals
const uri = location.pathname;
this.setState({
uri: uri,
});
if (uri === "/") {
this.setState({selectedMenuKey: "/"});
} else if (uri.includes("/payments")) {
this.setState({selectedMenuKey: "/payments"});
} else if (uri.includes("/contact")) {
this.setState({selectedMenuKey: "/contact"});
} else if (uri.includes("/competitions")) {
this.setState({selectedMenuKey: "/competitions"});
} else if (uri.includes("/conferences")) {
this.setState({selectedMenuKey: "/conferences"});
} else if (uri.includes("/code")) {
this.setState({selectedMenuKey: "/code"});
} else if (uri.includes("/submissions")) {
this.setState({selectedMenuKey: "/submissions"});
} else if (uri.includes("/all-pays")) {
this.setState({selectedMenuKey: "/all-pays"});
} else if (uri.includes("/rooms")) {
this.setState({selectedMenuKey: "/rooms"});
} else if (uri.includes("/public-rooms")) {
this.setState({selectedMenuKey: "/public-rooms"});
} else {
this.setState({selectedMenuKey: "/"});
}
}
onUpdateAccount(account) {
this.setState({
account: account,
});
}
setLanguage(account) {
// let language = account?.language;
const language = localStorage.getItem("language");
if (language !== "" && language !== i18next.language) {
Setting.setLanguage(language);
}
}
getAccount() {
AccountBackend.getAccount()
.then((res) => {
if (res.status === "error" && res.msg === "you have signed in from another place, this session has been ended") {
this.setState({
isDupSession: true,
});
return;
}
const account = res.data;
if (account !== null) {
this.setLanguage(account);
}
this.setState({
account: account,
});
});
}
signout() {
AccountBackend.signout()
.then((res) => {
if (res.status === "ok") {
this.setState({
account: null,
});
Setting.showMessage("success", "Successfully signed out, redirected to homepage");
Setting.goToLink("/");
// this.props.history.push("/");
} else {
Setting.showMessage("error", `Signout failed: ${res.msg}`);
}
});
}
handleRightDropdownClick(e) {
if (e.key === "/account") {
Setting.openLink(Setting.getMyProfileUrl(this.state.account));
} else if (e.key === "/logout") {
this.signout();
}
}
renderAvatar() {
if (this.state.account.avatar === "") {
return (
<Avatar style={{backgroundColor: Setting.getAvatarColor(this.state.account.name), verticalAlign: "middle"}} size="large">
{Setting.getShortName(this.state.account.name)}
</Avatar>
);
} else {
return (
<Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large">
{Setting.getShortName(this.state.account.name)}
</Avatar>
);
}
}
renderRightDropdown() {
const menu = (
<Menu onClick={this.handleRightDropdownClick.bind(this)}>
<Menu.Item key="/account">
<SettingOutlined />
{i18next.t("account:My Account")}
</Menu.Item>
<Menu.Item key="/logout">
<LogoutOutlined />
{i18next.t("account:Sign Out")}
</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu}>
<div className="top-right-button">
&nbsp;
&nbsp;
{
this.renderAvatar()
}
&nbsp;
&nbsp;
{Setting.isMobile() ? null : Setting.getShortName(this.state.account.displayName)} &nbsp; <DownOutlined />
&nbsp;
&nbsp;
&nbsp;
</div>
</Dropdown>
);
}
renderAccount() {
const res = [];
if (this.state.account === undefined) {
return null;
} else if (this.state.account === null) {
res.push(
<div key="/signin">
<a href={Setting.getSigninUrl()} className="signin-button">
{i18next.t("account:Sign In")}
</a>
</div>
);
res.push(
<div key="/signup" style={{marginRight: "1rem"}}>
<a href={Setting.getSignupUrl()} className="signup-button">
{i18next.t("account:Sign Up")}
</a>
</div>
);
} else {
res.push(this.renderRightDropdown());
}
return res;
}
renderMenu() {
const res = [];
res.push(
<Menu.Item key="/">
<Link to="/">
{i18next.t("general:Home")}
</Link>
</Menu.Item>
);
if (this.state.account === null || this.state.account === undefined) {
if (!Conf.IsConferenceMode) {
res.push(
<Menu.Item key="/competitions">
<Link to="/competitions">
{i18next.t("general:Competitions")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/public-rooms">
<Link to="/public-rooms">
{i18next.t("general:Public Rooms")}
</Link>
</Menu.Item>
);
return res;
}
res.push(
<Menu.Item key="/payments">
<Link to="/payments">
{i18next.t("general:Payments")}
</Link>
</Menu.Item>
);
if (!Conf.IsConferenceMode) {
res.push(
<Menu.Item key="/competitions">
<Link to="/competitions">
{i18next.t("general:Competitions")}
</Link>
</Menu.Item>
);
}
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/conferences">
<Link to="/conferences">
{i18next.t("general:Conferences")}
</Link>
</Menu.Item>
);
if (!Conf.IsConferenceMode) {
res.push(
<Menu.Item key="/code">
<Link to="/code">
{i18next.t("general:Code")}
</Link>
</Menu.Item>
);
}
}
res.push(
<Menu.Item key="/submissions">
<Link to="/submissions">
{i18next.t("general:Submissions")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/videos">
<a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", "/resources")}>
{i18next.t("general:Videos")}
</a>
</Menu.Item>
);
res.push(
<Menu.Item key="/contact">
<Link to="/contact">
{i18next.t("general:Service Desk")}
</Link>
</Menu.Item>
);
if (Setting.isAdminUser(this.state.account) || Setting.isEditorUser(this.state.account)) {
res.push(
<Menu.Item key="/all-pays">
<Link to="/all-pays">
{i18next.t("general:All Payments")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/users">
<a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", "/users")}>
{i18next.t("general:Users")}
</a>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/rooms">
<Link to="/rooms">
{i18next.t("general:Rooms")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/public-rooms">
<Link to="/public-rooms">
{i18next.t("general:Public Rooms")}
</Link>
</Menu.Item>
);
return res;
}
renderDupSessionModal() {
if (!this.state.isDupSession) {
return null;
}
const handleOk = () => {
this.signout();
};
return (
<Modal
title={
<div>
<CloseCircleTwoTone twoToneColor="rgb(255,77,79)" />
{" " + i18next.t("general:You have signed in from another place...")}
</div>
}
visible={true}
cancelButtonProps={{
style: {
display: "none",
},
}}
onOk={handleOk}
onCancel={() => {}}
okText={i18next.t("general:Sign Out")}
closable={false}
>
<div>
{i18next.t("general:Only one session is allowed to access this page. You have signed in from another place, this session has been ended automatically. If you want to sign in with this device again, please click 'Sign Out', and sign in with this device, the other session will be kicked off.")}
</div>
</Modal>
);
}
renderHomeIfSignedIn(component) {
if (this.state.account !== null && this.state.account !== undefined) {
return <Redirect to="/" />;
} else {
return component;
}
}
renderSigninIfNotSignedIn(component) {
if (this.state.account === null) {
sessionStorage.setItem("from", window.location.pathname);
return <Redirect to="/signin" />;
} else if (this.state.account === undefined) {
return null;
} else {
return component;
}
}
renderContact() {
if (Conf.ContactInfo.length === 0) {
return null;
}
return (
<Popover content={
<div style={{width: "300px"}}>
<List
itemLayout="horizontal"
dataSource={Conf.ContactInfo}
renderItem={item => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={item.logo} />}
title={<a target="_blank" rel="noreferrer" href={item.joinLink}>{item.title}</a>}
description={
<div>
<Row>
<Col span={10}>
{i18next.t("room:Meeting ID")}:
</Col>
<Col span={14}>
{item.meetingId}
</Col>
</Row>
<Row>
<Col span={10}>
{i18next.t("room:Passcode")}:
</Col>
<Col span={14}>
{item.passcode}
</Col>
</Row>
</div>
}
/>
</List.Item>
)}
/>
</div>
} trigger="click">
<div key="/support" style={{marginRight: "1rem"}}>
<a href={Setting.getSignupUrl()} className="signup-button">
{i18next.t("room:Support")}
</a>
</div>
</Popover>
);
}
renderContent() {
return (
<div>
<Header style={{padding: "0", marginBottom: "3px", display: "flex", flexWrap: "nowrap", backgroundColor: "white", borderBottom: "1px solid #f0f0f0"}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
}
<Menu
// theme="dark"
mode={"horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", width: "70%", marginRight: "auto", border: "none"}}
>
{
this.renderMenu()
}
</Menu>
<div
className="top-right-layout"
>
{
this.renderContact()
}
<SelectLanguageBox />
{
this.renderAccount()
}
</div>
</Header>
<Switch>
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/" render={(props) => <HomePage account={this.state.account} {...props} />} />
<Route exact path="/signin" render={(props) => this.renderHomeIfSignedIn(<SigninPage {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderSigninIfNotSignedIn(<PaymentPage account={this.state.account} {...props} />)} />
<Route exact path="/contact" render={(props) => this.renderSigninIfNotSignedIn(<ContactPage account={this.state.account} {...props} />)} />
<Route path="/competitions" render={(props) => <CompetitionListPage account={this.state.account} {...props} />} />
<Route exact path="/conferences" render={(props) => this.renderSigninIfNotSignedIn(<ConferenceListPage account={this.state.account} {...props} />)} />
<Route exact path="/conferences/:conferenceName" render={(props) => this.renderSigninIfNotSignedIn(<ConferenceEditPage account={this.state.account} {...props} />)} />
<Route exact path="/code" render={(props) => <CodeListPage account={this.state.account} {...props} />} />
<Route exact path="/code/:codeName" render={(props) => this.renderSigninIfNotSignedIn(<CodeEditPage account={this.state.account} {...props} />)} />
<Route exact path="/submissions" render={(props) => this.renderSigninIfNotSignedIn(<SubmissionListPage account={this.state.account} {...props} />)} />
<Route exact path="/submissions/:userName/:submissionName" render={(props) => this.renderSigninIfNotSignedIn(<SubmissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/all-pays" render={(props) => this.renderSigninIfNotSignedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/rooms" render={(props) => this.renderSigninIfNotSignedIn(<RoomListPage key={"rooms"} account={this.state.account} {...props} />)} />
<Route exact path="/rooms/:userName/:roomName" render={(props) => this.renderSigninIfNotSignedIn(<RoomEditPage account={this.state.account} {...props} />)} />
<Route exact path="/rooms/:userName/:roomName/view" render={(props) => <RoomPage account={this.state.account} {...props} />} />
<Route exact path="/rooms/:userName/:roomName/:slotName/view" render={(props) => <RoomPage account={this.state.account} {...props} />} />
<Route exact path="/public-rooms" render={(props) => <RoomListPage key={"public-rooms"} account={this.state.account} isPublic={true} {...props} />} />
<Route exact path="/:menu+" render={(props) => <HomePage account={this.state.account} {...props} />} />
</Switch>
</div>
);
}
renderFooter() {
// How to keep your footer where it belongs ?
// https://www.freecodecamp.org/neyarnws/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c/
return (
<Footer id="footer" style={
{
borderTop: "1px solid #e8e8e8",
backgroundColor: "white",
textAlign: "center",
}
}>
{
Setting.getLanguage() !== "en" ? (
<React.Fragment>
{Conf.title}
</React.Fragment>
) : (
<React.Fragment>
{Conf.titleEn}
</React.Fragment>
)
}
</Footer>
);
}
render() {
return (
<div id="parent-area">
<BackTop />
<div id="content-wrap">
{
this.renderContent()
}
</div>
{
this.renderFooter()
}
{
this.renderDupSessionModal()
}
</div>
);
}
}
export default withRouter(withTranslation()(App));

119
web/src/App.less Normal file
View File

@ -0,0 +1,119 @@
@import '~antd/dist/antd.less';
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #09d3ac;
}
#parent-area {
position: relative;
min-height: 100vh;
}
#content-wrap {
padding-bottom: 70px; /* Footer height */
}
#footer {
position: absolute;
bottom: 0;
width: 100%;
height: 70px; /* Footer height */
}
.ant-table-body {
overflow-y: hidden !important
}
.language-box {
background: url("https://cdn.casbin.org/img/muti_language.svg");
background-size: 25px, 25px;
background-position: center;
background-repeat: no-repeat;
width: 45px;
height: 65px;
float: right;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.top-right-layout {
flex-shrink: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
gap: 1rem;
.top-right-button {
flex-shrink: 0;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
}
.signin-button {
color: black !important;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 1rem;
transition: all 0.3s ease-in-out;
&:hover {
background-color: rgba(32, 33, 36, 0.1);
}
}
.signup-button {
color: white !important;
background-color: #7f5de3;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 1rem;
transition: all 0.3s ease-in-out;
&:hover {
box-shadow: 0rem 0rem 0.3rem #7f5de3;
}
}
.signin-button {
color: black !important;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 1rem;
transition: all 0.3s ease-in-out;
}
.signin-button:hover {
background-color: rgba(32, 33, 36, 0.1);
}
.signup-button {
color: white !important;
background-color: #7f5de3;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 1rem;
transition: all 0.3s ease-in-out;
}
.signup-button:hover {
box-shadow: 0rem 0rem 0.3rem #7f5de3;
}

23
web/src/App.test.js Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

85
web/src/AuthCallback.js Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Result, Spin} from "antd";
import {withRouter} from "react-router-dom";
import * as Setting from "./Setting";
class AuthCallback extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
msg: null,
};
}
UNSAFE_componentWillMount() {
this.login();
}
getFromLink() {
const from = sessionStorage.getItem("from");
if (from === null) {
return "/";
}
return from;
}
login() {
Setting.signin().then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", "Logged in successfully");
const link = this.getFromLink();
Setting.goToLink(link);
} else {
this.setState({
msg: res.msg,
});
}
});
}
render() {
return (
<div style={{textAlign: "center"}}>
{this.state.msg === null ? (
<Spin
size="large"
tip="Signing in..."
style={{paddingTop: "10%"}}
/>
) : (
<div style={{display: "inline"}}>
<Result
status="error"
title="Login Error"
subTitle={this.state.msg}
extra={[
<Button type="primary" key="details">
Details
</Button>,
<Button key="help">Help</Button>,
]}
/>
</div>
)}
</div>
);
}
}
export default withRouter(AuthCallback);

196
web/src/AuthorTable.js Normal file
View File

@ -0,0 +1,196 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
class AuthorTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
parseField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateField(table, index, key, value) {
value = this.parseField(key, value);
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {no: table.length, name: `New Author - ${table.length}`, data: []};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("general:No."),
dataIndex: "no",
key: "no",
width: "80px",
render: (text, record, index) => {
return (index + 1);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "250px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "name", e.target.value);
}} />
);
},
},
{
title: i18next.t("payment:Affiliation"),
dataIndex: "affiliation",
key: "affiliation",
// width: '400px',
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "affiliation", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Email"),
dataIndex: "email",
key: "email",
width: "250px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "email", e.target.value);
}} />
);
},
},
{
title: i18next.t("submission:Is notified"),
dataIndex: "isNotified",
key: "isNotified",
width: "160px",
render: (text, record, index) => {
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, "isNotified", checked);
}} />
);
},
},
{
title: i18next.t("submission:Is corresponding"),
dataIndex: "isCorresponding",
key: "isCorresponding",
width: "160px",
render: (text, record, index) => {
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, "isCorresponding", checked);
}} />
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={"Up"}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Down"}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Delete"}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default AuthorTable;

149
web/src/CodeEditPage.js Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row} from "antd";
import * as CodeBackend from "./backend/CodeBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
class CodeEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
codeName: props.match.params.codeName,
code: null,
};
}
UNSAFE_componentWillMount() {
this.getCode();
}
getCode() {
CodeBackend.getCode(this.props.account.name, this.state.codeName)
.then((code) => {
this.setState({
code: code,
});
});
}
parseCodeField(key, value) {
if (["score"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateCodeField(key, value) {
value = this.parseCodeField(key, value);
const code = this.state.code;
code[key] = value;
this.setState({
code: code,
});
}
renderCode() {
return (
<Card size="small" title={
<div>
{i18next.t("code:Edit Code")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={this.submitCodeEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Name")}:
</Col>
<Col span={22} >
<Input value={this.state.code.name} onChange={e => {
this.updateCodeField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Display name")}:
</Col>
<Col span={22} >
<Input value={this.state.code.displayName} onChange={e => {
this.updateCodeField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("code:Notebook")}:
</Col>
<Col span={22} >
<Input value={this.state.code.notebook} onChange={e => {
this.updateCodeField("notebook", e.target.value);
}} />
</Col>
</Row>
</Card>
);
}
submitCodeEdit() {
const code = Setting.deepCopy(this.state.code);
CodeBackend.updateCode(this.state.code.owner, this.state.codeName, code)
.then((res) => {
if (res) {
Setting.showMessage("success", "Successfully saved");
this.setState({
codeName: this.state.code.name,
});
this.props.history.push(`/code/${this.state.code.name}`);
} else {
Setting.showMessage("error", "failed to save: server side failure");
this.updateCodeField("name", this.state.codeName);
}
})
.catch(error => {
Setting.showMessage("error", `failed to save: ${error}`);
});
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.state.code !== null ? this.renderCode() : null
}
</Col>
<Col span={1}>
</Col>
</Row>
<Row style={{margin: 10}}>
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large" onClick={this.submitCodeEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>
);
}
}
export default CodeEditPage;

389
web/src/CodeListPage.js Normal file
View File

@ -0,0 +1,389 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {createRef} from "react";
import moment from "moment";
import * as Setting from "./Setting";
import * as CodeBackend from "./backend/CodeBackend";
import i18next from "i18next";
import "./CodeListPage.less";
import {BookOutlined, CopyOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, FilterOutlined, MoreOutlined, PlusOutlined, SearchOutlined} from "@ant-design/icons";
import {Pagination, Popover} from "antd";
import {mapStringToTag} from "./Utils";
/**
*
* @param {CodeBackend.Code} code
* @param {CodeListPage} page
* @returns {{icon: JSX.Element, label: string, fn: () => void, ownerOnly?: boolean }[]}
*/
const codeActions = (code, page) => ([
{icon: <EditOutlined />, label: "code:Edit", fn: () => Setting.goToLink(`code/${code.name}`), ownerOnly: true},
{icon: <CopyOutlined />, label: "code:Copy and Edit", fn: () => { }},
{icon: <DownloadOutlined />, label: "code:Download", fn: () => { }},
{icon: <BookOutlined />, label: "code:Bookmark", fn: () => { }},
{icon: <DeleteOutlined />, label: "code:Delete", fn: () => page.deleteCode(code), ownerOnly: true},
]);
class CodeListPage extends React.Component {
constructor(props) {
super(props);
/**
* @type {{ classes: { account: any }, codes: CodeBackend.Code[], globalCodes: CodeBackend.Code[], isSearching: boolean, topActivate: boolean, searchKeywords: string[], searchTags: Setting.CodeTag[], searchResultPage: number, displayMyWork: boolean }}
*/
this.state = {
classes: props,
codes: [],
globalCodes: [],
isSearching: false,
topActivate: false,
searchKeywords: [],
searchTags: [],
searchResultPage: 1,
displayMyWork: false,
};
this.searchAreaRef = createRef();
}
/**
* @param { (...arg:any) => any } fn
*/
doIfSignin(fn) {
return () => {
if (this.props.account !== null && this.props.account !== undefined) {
fn();
return;
}
Setting.goToLink("/signin");
};
}
UNSAFE_componentWillMount() {
this.getGlobalCodes();
if (this.props.account !== null && this.props.account !== undefined) {
this.getCodes();
}
this.listenScroll();
}
listenScroll() {
window.addEventListener("scroll", () => this.setState({
topActivate: this.searchAreaRef.current ? this.searchAreaRef.current?.getBoundingClientRect().top <= 0 : false,
}));
}
getGlobalCodes() {
CodeBackend.getGlobalCodes()
.then(res => {
this.setState({
globalCodes: res,
});
});
}
getCodes() {
CodeBackend.getCodes(this.props.account.name)
.then((res) => {
this.setState({
codes: res,
});
});
}
/**
*
* @returns {CodeBackend.Code}
*/
newCode() {
return {
owner: this.props.account.name,
name: `code_${this.state.codes.length}`,
createdTime: moment().format(),
displayName: `New Code - ${this.state.codes.length}`,
notebook: "code",
tags: [],
imgUrl: "",
};
}
addCode() {
const newCode = this.newCode();
CodeBackend.addCode(newCode)
.then((res) => {
Setting.showMessage("success", "Code added successfully");
Setting.goToLink(`code/${newCode.name}`);
}
)
.catch(error => {
Setting.showMessage("error", `Code failed to add: ${error}`);
});
}
/**
*
* @param {CodeBackend.Code} code
*/
deleteCode(code) {
CodeBackend.deleteCode(code)
.then((res) => {
Setting.showMessage("success", "Code deleted successfully");
this.setState({
codes: this.state.codes.filter(existedCode => existedCode.name !== code.name),
globalCodes: this.state.globalCodes.filter(existedCode => existedCode.name !== code.name),
});
}
)
.catch(error => {
Setting.showMessage("error", `Code failed to delete: ${error}`);
});
}
/**
*
* @param {string[] | null} keywords
* @param {Setting.CodeTag[] | null} tags
*/
searchCode(keywords, tags) {
keywords = keywords ?? this.state.searchKeywords;
tags = tags ?? this.state.searchTags;
// eslint-disable-next-line
console.log(keywords, tags);
this.setState({
isSearching: keywords !== null && keywords.length !== 0 || tags.length !== 0,
});
}
/**
*
* @param {Setting.CodeTag} tag
*/
tagOnClicAction(tag) {
if (this.state.searchTags.find(existedTag => existedTag.label === tag.label)) {
const tags = this.state.searchTags.filter(existedTag => existedTag.label !== tag.label);
this.setState({
searchTags: tags,
searchResultPage: 1,
});
this.searchCode(null, tags);
return;
}
const tags = [...new Set([...this.state.searchTags, tag])];
this.setState({
searchTags: tags,
searchResultPage: 1,
});
this.searchCode(null, tags);
}
/**
*
* @param {string} value
*/
searchOnInputAction(value) {
const keywords = value.trim().length === 0 ? [] : value.split(" ");
this.setState({
searchKeywords: keywords,
});
this.searchCode(keywords, null);
}
renderCodeActionsMenu(code) {
return <ul className="code-actions-menu">
{codeActions(code, this).filter(action => code.owner === this.props.account?.name ? true : !action.ownerOnly).map(({icon, label, fn}) =>
<li key={code.label + label} onClick={e => {fn(); e.stopPropagation();}}>
{icon}
{i18next.t(label)}
</li>)}
</ul>;
}
renderTopBanner() {
return <div className="top-banner">
<h1>{
this.state.displayMyWork ? i18next.t("general:Your Code") :
i18next.t("general:Code")}
</h1>
<p>{!this.state.displayMyWork && i18next.t("code:Explore and run machine learning code with Confita Notebooks.")}</p>
<div>
<button onClick={this.doIfSignin(this.addCode.bind(this))}><PlusOutlined style={{fontSize: "large"}} />{i18next.t("code:New Notebook")}</button>
{this.state.classes.account && <button onClick={() => this.setState({displayMyWork: !this.state.displayMyWork})}>{i18next.t(this.state.displayMyWork ? "code:Public Notebook" : "code:Your work")}</button>}
</div>
</div>;
}
/**
*
* @param {{ tags: Setting.CodeTag[] }} props
*/
renderSearchArea({tags}) {
/**
*
* @type {React.FC<{ category: string, tags: Setting.CodeTag[] }>}
*/
const FilterContentRow = ({category, tags}) => <div>
<p>{category.toUpperCase()}</p>
<ul>
{tags.map(tag => <li className={`${this.state.searchTags.find(existedTag => existedTag.label === tag.label) ? "clicked" : ""}`} key={tag.label} onClick={() => this.tagOnClicAction(tag)}>{tag.label}</li>)}
</ul>
</div>;
const visibleEntries = Object.entries(Setting.getCodeTags()).filter(([category]) => category !== "general");
const FilterContent = () => <div className="filter-content">
{
visibleEntries
.map(([category, tags]) => ({category, tags: tags.map(mapStringToTag)}))
.map(({category, tags}) => <FilterContentRow key={category} category={category} tags={tags} />)
}
</div>;
return <div className={`search-area ${this.state.topActivate ? "top-activate" : ""}`} ref={this.searchAreaRef}>
<div className="search-input">
<SearchOutlined style={{fontSize: "x-large", margin: "0.1rem 1rem 0.1rem 1rem"}} />
<input placeholder={i18next.t(`code:Search ${this.state.displayMyWork ? "" : "public"} notebooks`)} onInput={e => this.searchOnInputAction(e.currentTarget.value)}></input>
<Popover content={<FilterContent />} trigger={"click"} placement={"bottomLeft"}>
<div>
<FilterOutlined style={{fontSize: "large"}} />
Filters
</div>
</Popover>
</div>
<div className="tags-cloud">
{tags.map(tag => <button className={`${this.state.searchTags.find(existedTag => existedTag.label === tag.label) ? "clicked" : ""}`} key={tag.label} onClick={() => this.tagOnClicAction(tag)}>{tag.label}</button>)}
</div>
</div>;
}
/**
*
* @param {{ rows: { tag: Setting.CodeTag, codes: CodeBackend.Code[] }[] }} props
*/
renderCodeList({rows}) {
/**
*
* @type {React.FC<{code: CodeBackend.Code}>}
*/
const CodeListRowItem = ({code}) => {
return <li className="code-list-row-item" onClick={() => Setting.goToLink(`code/${code.name}`)}>
<img src={code.imgUrl} />
<div>
<p>{code.displayName}</p>
<p>{code.createdTime}</p>
<div>
<Popover trigger={"clicked"} placement="bottomLeft" content={this.renderCodeActionsMenu(code)}>
<button onClick={e => e.stopPropagation()}>
<MoreOutlined />
</button>
</Popover>
</div>
</div>
<div>
<p>{code.owner}</p>
</div>
</li>;
};
/**
*
* @type {React.FC<{ tag: Setting.CodeTag ,codes: CodeBackend.Code[]}>}
*/
const CodeListRow = ({tag, codes}) => {
return <li className="code-list-row">
<h2>{tag.label}</h2>
<button onClick={() => this.tagOnClicAction(tag)}>See all&nbsp;({codes.length})</button>
<ul>{codes.slice(0, 6).map(code => <CodeListRowItem key={code.name} code={code} />)}</ul>
</li>;
};
/**
*
* @type {React.FC<{ rows: { tag: Setting.CodeTag, codes: CodeBackend.Code[] }[] }>}
*/
const CodeList = ({rows}) => {
return <ul className="code-list">
{rows.map(({tag, codes}) => <CodeListRow key={tag.label} tag={tag} codes={codes} />)}
</ul>;
};
return <CodeList rows={rows} />;
}
/**
*
* @param {{ codes: CodeBackend.Code[], keywords: string[], tags: Setting.CodeTag[] }} props
*/
renderSearchResultList({codes, keywords, tags}) {
/**
*
* @type {React.FC<{ code: CodeBackend.Code }>}
*/
const SearchResultListRow = ({code}) => {
return <li onClick={() => Setting.goToLink(`code/${code.name}`)}>
<img src={code.imgUrl} alt={code.displayName + "image"} />
<div>
<p>{code.displayName}</p>
<p>{code.createdTime}</p>
</div>
<div>
<p>{code.owner}</p>
<Popover trigger={"clicked"} placement="bottomLeft" content={this.renderCodeActionsMenu(code)}>
<button onClick={e => e.stopPropagation()}>
<MoreOutlined style={{transform: "rotate(90deg)"}} />
</button>
</Popover>
</div>
</li>;
};
/**
*
* @type {React.FC<{ codes: CodeBackend.Code[] }>}
*/
const SearchResultList = ({codes}) => {
const pageSize = 20;
const page = this.state.searchResultPage;
return <div className="search-result-list">
<h2>Result ({codes.length})</h2>
<ul>{codes.slice((page - 1) * pageSize, page * pageSize).map(code => <SearchResultListRow key={code.name} code={code} />)}</ul>
<Pagination showSizeChanger={false} style={{display: "flex", justifyContent: "center", padding: "1rem"}} current={page} total={codes.length} pageSize={pageSize} onChange={page => this.setState({searchResultPage: page})} />
</div>;
};
const result = codes
.filter(code => (this.state.displayMyWork && this.props.account) ? code.owner === this.props.account.name : true)
.filter(code => keywords
.filter(keyword => code.displayName.toLowerCase().indexOf(keyword.toLowerCase()) !== -1).length !== 0 || keywords.length === 0)
.filter(code => tags
.filter(tag => tag.filter(code)).length === tags.length);
return <SearchResultList codes={result} />;
}
render() {
const tags = Object.values(Setting.getCodeTags()).flat().map(mapStringToTag);
const rows = tags.map(tag => ({tag, codes: this.state.globalCodes?.filter(tag.filter) ?? []})).filter(row => row.codes.length !== 0);
return <div className="code-list-page-container">
{this.renderTopBanner()}
{this.renderSearchArea({tags})}
{this.state.isSearching || this.state.displayMyWork ? this.renderSearchResultList({codes: this.state.codes, keywords: this.state.searchKeywords, tags: this.state.searchTags}) : this.renderCodeList({rows})}
</div>;
}
}
export default CodeListPage;

400
web/src/CodeListPage.less Normal file
View File

@ -0,0 +1,400 @@
.tag {
display: flex;
justify-content: center;
align-items: center;
padding: 0.3rem 0.7rem 0.3rem 0.7rem;
border: 0.1rem solid rgb(189, 193, 198);
border-radius: 2rem;
background-color: transparent;
cursor: pointer;
&:hover {
border-color: #7f5de3;
}
}
.tag-clicked {
background-color: #7f5de3;
color: white;
}
.code-actions-menu {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: stretch;
font-size: large;
>li {
display: flex;
gap: 1rem;
align-items: center;
border-radius: 1rem;
padding: 0.3rem 1rem 0.3rem 1rem;
cursor: pointer;
&:hover {
background-color: #ededed;
}
}
}
.filter-content {
>div {
>p {
font-size: x-small;
font-weight: bold;
}
>ul {
display: flex;
padding-left: 0;
gap: 0.5rem;
>li {
list-style: none;
.tag();
&.clicked {
.tag-clicked();
}
}
}
}
}
.code-list-page-container {
display: grid;
grid-template-columns: minmax(1.5rem, auto) minmax(0, 1024px) minmax(1.5rem, auto);
grid-row-gap: 3rem;
.top-banner {
grid-column: 2;
display: flex;
flex-direction: column;
h1 {
font-weight: 700;
font-size: 36px;
}
p {
font-size: 16px;
font-weight: 400;
color: gray;
}
div {
display: flex;
gap: 1rem;
button {
border: none;
border-radius: 2rem;
padding: 0.5rem 1.5rem 0.5rem 1.5rem;
display: flex;
align-items: center;
font-weight: bold;
cursor: pointer;
}
button:nth-child(1) {
background-color: #7f5de3;
color: white;
gap: 0.7rem;
transition: box-shadow 0.3s ease-in-out;
&:hover {
box-shadow: 0 0 0.3rem #7f5de3;
}
}
button:nth-child(2) {
background-color: transparent;
border: solid;
border-width: 0.1rem;
border-color: rgb(189, 193, 198);
transition: border-color 0.3s ease-in-out;
&:hover {
border-color: #7f5de3;
}
}
}
}
.top-activate {
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.3);
margin: 0 !important;
padding: 1rem 0 1rem 0;
}
.search-area {
grid-column: 1 / span 3;
position: sticky;
top: 0;
display: grid;
grid-template-columns: minmax(1.5rem, auto) minmax(0, 1024px) minmax(1.5rem, auto);
grid-row-gap: 1rem;
z-index: 10;
background-color: white;
.search-input {
grid-column: 2;
width: 100%;
height: 3rem;
display: flex;
justify-content: flex-start;
align-items: center;
border: 0.1rem solid rgb(189, 193, 198);
border-radius: 2rem;
transition: border-color 0.3s ease-in-out;
&:focus-within {
border-color: #7f5de3;
}
input {
flex: 1;
border: none !important;
outline: none !important;
}
>div {
display: flex;
align-items: center;
margin: 0.1rem 0.5rem 0.1rem 0.5rem;
padding: 0.3rem 1rem 0.3rem 1rem;
border-radius: 2rem;
gap: 0.5rem;
font-weight: bold;
font-size: medium;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
&:hover {
background-color: rgb(189, 193, 198);
}
}
}
.tags-cloud {
grid-column: 2;
display: flex;
flex-wrap: wrap;
line-height: 1.3rem;
gap: 0.5rem;
button {
.tag();
&.clicked {
.tag-clicked();
}
}
}
}
.code-list {
grid-column: 2;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-left: 0;
.code-list-row {
border-bottom: 0.05rem solid rgb(189, 193, 198);
margin-top: 2rem;
padding-bottom: 2rem;
display: grid;
grid-template-columns: repeat(1fr);
grid-template-rows: 3rem auto;
align-items: center;
>h2 {
grid-column: 1 / span 2;
font-size: large;
font-weight: bold;
}
>button {
grid-column: 3;
justify-self: end;
border: none;
border-radius: 1rem;
padding: 0.3rem 1rem 0.3rem 1rem;
font-weight: bold;
font-size: medium;
cursor: pointer;
&:hover {
background-color: rgb(189, 193, 198);
}
}
>ul {
grid-column: 1 / span 3;
grid-row: 2;
margin-top: 1rem;
width: 100%;
height: 300px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-rows: 300px;
grid-row-gap: 100vh;
overflow: hidden;
grid-column-gap: 1rem;
padding-left: 0;
}
.code-list-row-item {
border: 0.1rem solid rgb(189, 193, 198);
border-radius: 1rem;
display: flex;
flex-direction: column;
align-content: stretch;
gap: 1rem;
overflow: hidden;
cursor: pointer;
&:hover {
box-shadow: 0 0 0.5rem rgb(189, 193, 198);
}
>img {
height: 40%;
}
>div:nth-of-type(1) {
padding-left: 0.5rem;
padding-right: 0.5rem;
margin-bottom: auto;
display: grid;
grid-template-columns: 85% 15%;
>p {
grid-column: 1;
margin-bottom: 0;
}
>p:nth-child(1) {
font-size: medium;
font-weight: bold;
}
>p:nth-child(2) {
font-size: small;
}
>div {
grid-row: 1;
grid-column: 2;
>button {
background-color: transparent;
border: none;
border-radius: 1rem;
font-size: large;
cursor: pointer;
&:hover {
background-color: rgb(189, 193, 198);
}
}
}
}
>div:nth-of-type(2) {
padding: 0.5rem;
border: solid rgb(189, 193, 198);
border-width: 0.1rem 0 0 0;
display: flex;
justify-content: flex-end;
>p {
margin: 0;
}
}
}
}
.code-list-row:last-child {
border: none;
}
}
.search-result-list {
grid-column: 2;
>h2 {
font-weight: bold;
}
>ul:nth-child(2) {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 0;
>li {
width: 100%;
height: 5rem;
padding: 1rem;
border-bottom: 0.1rem solid #ededed;
list-style: none;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background-color: #ededed;
}
>img {
flex-shrink: 0;
border-radius: 100%;
width: 4rem;
height: 4rem;
}
>div:nth-of-type(1) {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
>p {
padding: 0;
margin: 0;
}
>p:nth-child(1) {
font-size: medium;
font-weight: bold;
}
}
>div:nth-of-type(2) {
display: flex;
flex-direction: column;
justify-content: center;
>button {
background-color: transparent;
border: none;
border-radius: 1rem;
cursor: pointer;
&:hover {
background-color: rgb(189, 193, 198);
}
}
}
}
}
}
}

View File

@ -0,0 +1,89 @@
// Copyright 2022 The Casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {withRouter} from "react-router-dom";
import {Card} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
import "./CompetitionCard.less";
const {Meta} = Card;
class CompetitionCard extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
renderCardMobile(competition) {
return (
<Card className="competition-card-mobile" bodyStyle={{padding: 0}} hoverable>
<img src={competition.carousels[0]} alt="logo" height={60} style={{marginBottom: "20px", padding: "10px"}} />
<Meta
title={competition.displayName}
description={competition.introduction}
/>
<br />
<div className="card-button-mobile">
{i18next.t("competition:Ongoing")}
</div>
</Card>
);
}
renderCard(competition) {
return (
<div className="competition-card-container">
<Card className="competition-card"
cover={<img alt="logo" src={competition.carousels[0]} style={{width: "100%", height: "auto", objectFit: "scale-down", padding: "10px"}} />}
bodyStyle={{margin: 0, padding: 0}}
hoverable
>
<div className="card-body">
<Meta title={competition.displayName} description={competition.introduction} />
</div>
<div className="card-bottom">
<div>
<p>{i18next.t("competition:Organizer")}</p>
<p>{competition.organizer}</p>
</div>
<div>
{i18next.t("competition:Ongoing")}
</div>
</div>
</Card>
</div>
);
}
renderContent() {
const competition = this.props.competition;
if (Setting.isMobile()) {
return this.renderCardMobile(competition);
} else {
return this.renderCard(competition);
}
}
render() {
return this.renderContent();
}
}
export default withRouter(CompetitionCard);

View File

@ -0,0 +1,82 @@
.card-button {
display: flex;
justify-content: center;
align-items: center;
background-color: #7F5DE3;
color: white;
cursor: pointer;
transition: opacity 0.3s ease-in-out;
&:hover {
opacity: 90%;
}
}
.competition-card-mobile {
margin: 0 0.5rem 1rem .5rem;
box-shadow: 0 0 0.3rem rgba(0, 0, 0, 0.3);
* {
padding-left: 1rem;
padding-right: 1rem;
}
.card-button-mobile {
width: 100%;
height: 3rem;
.card-button()
}
}
.competition-card-container {
width: 550px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
.competition-card {
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
img {
box-shadow: inset 0 0 3rem rgba(0, 0, 0, 0.1);
}
.card-body {
border-top: 0.1rem solid #ededed;
padding: 1rem 2rem 0 2rem;
}
.card-bottom {
border-top: 0.1rem solid #ededed;
margin-top: 1rem;
width: 100%;
height: 4rem;
display: flex;
>div:nth-child(1) {
padding-left: 2rem;
flex-grow: 2;
display: flex;
justify-content: flex-start;
align-items: center;
p {
height: fit-content;
margin-bottom: 0;
&:nth-child(1) {
padding-right: 1rem;
}
}
}
>div:nth-child(2) {
width: 10rem;
border-radius: 0 0 5px 0;
.card-button()
}
}
}
}

View File

@ -0,0 +1,165 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Menu, Pagination, Row} from "antd";
import * as Setting from "./Setting";
import CompetitionCard from "./CompetitionCard";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import "./CompetitionListPage.less";
import {Link} from "react-router-dom";
class CompetitionListPage extends React.Component {
constructor(props) {
super(props);
/**
* @type {{ classes: any, competitions: null | { carousels: string[], displayName: string, introduction: string, organizer: string }[], pageSize: number, pageNum: number, topActivate: boolean}}
*/
this.state = {
classes: props,
competitions: null,
pageSize: 10,
pageNum: 0,
topActivate: false,
};
this.navigationRef = React.createRef();
}
UNSAFE_componentWillMount() {
this.getCompetitions();
this.listenScroll();
}
listenScroll() {
window.addEventListener("scroll", () => this.setState({
topActivate: this.navigationRef.current ? this.navigationRef.current?.getBoundingClientRect().top <= 0 : false,
}));
}
getCompetitions() {
ConferenceBackend.getConferences("admin")
.then((res) => {
this.setState({
competitions: res,
});
});
}
renderCard(index, competition) {
return (
<CompetitionCard key={index} competition={competition} />
);
}
/**
* @param {{ link: string, label: string }} item
*/
renderNavigationButton(item) {
const {link, label} = item;
return <Link className={`navigation-button ${location.pathname.includes(link) ? "activate" : ""}`} key={link} to={`/competitions/${link}`}>
{label}
</Link>;
}
renderNavigationBar() {
const items = [{
link: "activate",
label: "Active",
}, {
link: "algo",
label: "算法大赛",
}, {
link: "innovantion",
label: "创新应用大赛",
}, {
link: "prog",
label: "程序设计大赛",
}, {
link: "learn",
label: "学习赛",
}, {
link: "visual",
label: "可视化大赛",
}, {
link: "ragnarok",
label: "诸神大战",
}];
if (Setting.isMobile()) {
return <Menu mode={"inline"}>{items.map(({link, label}) => <Menu.Item key={link}>{label}</Menu.Item>)}</Menu>;
}
return <div onClick={() => window.scrollTo({top: 0})} ref={this.navigationRef}><div className={`navigation-bar ${this.state.topActivate ? "top-activate" : ""}`}>{items.map(this.renderNavigationButton)}</div></div>;
}
renderCards() {
const competitions = this.state.competitions;
if (competitions === null) {
return null;
}
const idx = this.state.pageNum * this.state.pageSize;
const visibles = competitions.slice(idx, idx + this.state.pageSize);
return visibles.map((competition, i) => this.renderCard(i, competition));
}
/**
*
* @param {number} page
* @param {number} pageSize
*/
onPaginationChange(page, pageSize) {
this.setState({
pageNum: page - 1,
pageSize,
});
}
render() {
return (
<div className="competition-list-page">
{Setting.isMobile() ? null :
<div className="top-banner">
<p>Casbin 2022全球开源软件大赛</p>
<p>Casbin 是一个强大的高效的开源访问控制框架其权限管理机制支持多种访问控制模型</p>
</div>}
<Row>
<Col span={24}>
{
this.renderNavigationBar()
}
</Col>
</Row>
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<div className={Setting.isMobile() ? "competition-card-list-mobile" : "competition-card-list"}>
{
this.renderCards()
}
</div>
</div>
<Row style={{padding: "5rem"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<Pagination showQuickJumper current={this.state.pageNum + 1} total={this.state.competitions?.length} onChange={this.onPaginationChange.bind(this)} />
</Col>
</Row>
</div>
);
}
}
export default CompetitionListPage;

View File

@ -0,0 +1,98 @@
.competition-list-page {
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.top-banner {
width: 100%;
height: 281px;
background: linear-gradient(-45deg, #ee6a0e, #7f5de3, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
p {
text-overflow: ellipsis;
white-space: nowrap;
}
p:nth-child(1) {
font-size: xx-large;
}
p:nth-child(2) {
font-size: medium;
}
}
.top-activate {
position: fixed;
margin-top: 0 !important;
top: 0;
z-index: 10;
box-shadow: 0rem 0rem 1rem rgba(0, 0, 0, 0.3);
}
.navigation-bar {
margin-top: 0.3rem;
width: 100%;
height: 4rem;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
box-shadow: 0 0 0.1rem rgba(0, 0, 0, 0.3);
.activate {
background-color: #7f5de3 !important;
color: white !important;
}
.navigation-button {
flex-shrink: 0;
text-decoration: none;
background-color: white;
color: black;
height: 4rem;
padding: 1rem 2rem 1rem 2rem;
font-size: 1rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
.activate()
}
}
}
.competition-card-list {
max-width: 1500px;
margin-top: 5rem;
padding-left: 10rem;
padding-right: 10rem;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(auto-fill, 1fr);
grid-column-gap: 5rem;
grid-row-gap: 1rem;
justify-items: center;
align-items: center;
}
}

45
web/src/Conf.js Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export const AuthConfig = {
// serverUrl: "https://door.casbin.com",
serverUrl: "http://localhost:7001",
clientId: "4204b22726f5ff8c9efe",
appName: "app-confita",
organizationName: "casbin",
redirectPath: "/callback",
};
export const IsConferenceMode = true;
export const DefaultOwner = "admin";
export const DefaultConferenceName = "conference_0";
export const TestAffiliation = "";
export const paymentInfo = "";
export const paymentInfoEn = "";
export const isPaymentRequired = true;
export const ForceLanguage = "";
export const DefaultLanguage = "en";
// export const CasnodeEndpoint = "https://forum.casbin.com";
export const CasnodeEndpoint = "http://localhost:3000";
export const QrCodeImageUrl = "";
export const ContactInfo = [];
export const title = "Confita";
export const titleEn = "Confita (En)";

368
web/src/Conference.js Normal file
View File

@ -0,0 +1,368 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link, withRouter} from "react-router-dom";
import {Alert, Button, Col, Empty, Menu, Popover, Row, Space, Steps} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as Conf from "./Conf";
import {Helmet} from "react-helmet";
const {SubMenu} = Menu;
const {Step} = Steps;
class Conference extends React.Component {
static defaultProps = {
path: "/",
enableMenuPath: false,
};
constructor(props) {
super(props);
const selectedKey = this.getSelectedKey(props);
this.state = {
classes: props,
selectedKey: selectedKey || props.conference.defaultItem,
};
}
componentDidMount() {
this.updateSelectedKey();
}
componentDidUpdate() {
this.updateSelectedKey();
}
shouldComponentUpdate(nextProp, nextState) {
if (nextProp.language !== this.props.language) {
return true;
}
if (nextState.selectedKey !== this.state.selectedKey || nextProp.conference !== this.props.conference) {
const selectedTreeItem = this.getSelectedTreeItem(nextState.selectedKey, nextProp.conference.treeItems);
if (typeof this.props.onMatchTreeItem === "function") {
this.props.onMatchTreeItem(!!selectedTreeItem);
}
return true;
}
return false;
}
getSelectedKey(props) {
if (!props.enableMenuPath) {
return null;
}
const match = new RegExp(`${props.path}([^/]+)$`).exec(props.history.location.pathname);
if (match === null) {
return null;
}
return match[1];
}
updateSelectedKey() {
const selectedKey = this.getSelectedKey(this.props);
if (selectedKey === null) {
return;
}
this.setState({selectedKey});
}
renderMenu(treeItems) {
let mode;
if (!Setting.isMobile()) {
mode = "vertical";
} else {
mode = "horizontal";
}
const theme = "light";
// const theme = "dark";
return (
<Menu
// style={{ width: 256 }}
selectedKeys={[this.state.selectedKey]}
defaultOpenKeys={["sub1"]}
mode={mode}
theme={theme}
className={"conferenceMenu"}
style={{border: "1px solid rgb(240,240,240)"}}
onClick={this.handleClick}
>
{
treeItems.map((treeItem, i) => {
// if (i === 0) {
// return null;
// }
if (treeItem.children.length === 0) {
return (
<Menu.Item key={treeItem.titleEn} onClick={this.updateSelectedKey.bind(this)}>
<Link to={`${this.props.path}${treeItem.titleEn}`}>
{this.props.language !== "en" ? treeItem.title : treeItem.titleEn}
</Link>
</Menu.Item>
);
} else {
return (
<SubMenu key={treeItem.titleEn} title={this.props.language !== "en" ? treeItem.title : treeItem.titleEn}>
{
treeItem.children.map((treeItem2, i) => {
return (
<Menu.Item key={treeItem2.titleEn} onClick={this.updateSelectedKey.bind(this)}>
<Link to={`${this.props.path}${treeItem.titleEn}/${treeItem2.titleEn}`}>
{this.props.language !== "en" ? treeItem2.title : treeItem2.titleEn}
</Link>
</Menu.Item>
);
})
}
</SubMenu>
);
}
})
}
{/* <Menu.Item key="1" icon={<MailOutlined />}>*/}
{/* Introduction*/}
{/* </Menu.Item>*/}
{/* <Menu.Item key="1" icon={<MailOutlined />}>*/}
{/* Committees*/}
{/* </Menu.Item>*/}
{/* <Menu.Item key="1" icon={<MailOutlined />}>*/}
{/* Hosting Organizations*/}
{/* </Menu.Item>*/}
{/* <Menu.Item key="2" icon={<CalendarOutlined />}>*/}
{/* Navigation Two*/}
{/* </Menu.Item>*/}
{/* <SubMenu key="sub1" icon={<AppstoreOutlined />} title="Navigation Two">*/}
{/* <Menu.Item key="3">Option 3</Menu.Item>*/}
{/* <Menu.Item key="4">Option 4</Menu.Item>*/}
{/* <SubMenu key="sub1-2" title="Submenu">*/}
{/* <Menu.Item key="5">Option 5</Menu.Item>*/}
{/* <Menu.Item key="6">Option 6</Menu.Item>*/}
{/* </SubMenu>*/}
{/* </SubMenu>*/}
{/* <SubMenu key="sub2" icon={<SettingOutlined />} title="Navigation Three">*/}
{/* <Menu.Item key="7">Option 7</Menu.Item>*/}
{/* <Menu.Item key="8">Option 8</Menu.Item>*/}
{/* <Menu.Item key="9">Option 9</Menu.Item>*/}
{/* <Menu.Item key="10">Option 10</Menu.Item>*/}
{/* </SubMenu>*/}
{/* <Menu.Item key="link" icon={<LinkOutlined />}>*/}
{/* <a href="https://ant.design" target="_blank" rel="noopener noreferrer">*/}
{/* Ant Design*/}
{/* </a>*/}
{/* </Menu.Item>*/}
</Menu>
);
}
getSelectedTreeItem(selectedKey, treeItems) {
if (selectedKey === null) {
return null;
}
const res = treeItems.map(treeItem => {
if (treeItem.titleEn === selectedKey) {
return treeItem;
} else {
return this.getSelectedTreeItem(selectedKey, treeItem.children);
}
}).filter(treeItem => treeItem !== null);
if (res.length > 0) {
return res[0];
} else {
return null;
}
}
renderPage(treeItem) {
if (treeItem === undefined || treeItem === null) {
return (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
);
}
return (
<div>
{/* <div style={{textAlign: "center", fontSize: "x-large"}}>*/}
{/* {*/}
{/* treeItem.title*/}
{/* }*/}
{/* </div>*/}
<Helmet>
<title>{this.props.language !== "en" ? `${treeItem.title} - ${Conf.title}` : `${treeItem.titleEn} - ${Conf.titleEn}`}</title>
<meta
name="description"
content={this.state.language !== "en" ? treeItem.content : treeItem.contentEn}
/>
<meta
name="keywords"
content={this.state.language !== "en" ? treeItem.title : treeItem.titleEn}
/>
</Helmet>
<div style={{marginTop: "40px"}} dangerouslySetInnerHTML={{__html: this.props.language !== "en" ? treeItem.content : treeItem.contentEn}} />
</div>
);
}
renderCompetition(conference) {
if (conference.type !== "Competition") {
return null;
}
return (
<div style={{marginTop: "20px", marginBottom: "20px"}}>
<Alert
banner
showIcon={false}
message={
<h2>
<span style={{marginRight: "10px"}}>
{conference.displayName}
</span>
{
Setting.getTag(conference.displayState, "geekblue")
}
</h2>}
description={<div>
<h3>
{conference.introduction}
</h3>
<div>
{i18next.t("conference:Organizer")}: {conference.organizer}
</div>
<br />
{i18next.t("conference:Person count")} <span style={{marginLeft: "10px", fontSize: 20, color: "rgb(255,77,79)"}}>{conference.personCount}</span>
<span style={{float: "right"}}>
{
Setting.getTags(conference.tags)
}
</span>
<br />
<Steps style={{marginTop: "20px"}} current={1} progressDot={(dot, {status, index}) => {
return (
<Popover
content={
<span>
step {index} status: {status}
</span>
}>
{dot}
</Popover>
);
}}>
<Step title="报名" description="04/06-05/11" />
<Step title="初赛" description="06/01-07/31" />
<Step title="复赛" description="08/01-09/30" />
<Step title="决赛" description="09/30" />
</Steps>
</div>}
type="info"
action={
<Space direction="vertical" style={{textAlign: "center"}}>
&nbsp;
<div style={{fontSize: 30, color: "rgb(255,77,79)"}}>
{`${conference.bonus}`.replace("000", ",000")}
</div>
<Button style={{marginTop: "20px"}} shape={"round"} type="primary" onClick={() => this.props.history.push("/submissions")}>
{i18next.t("conference:Apple Now")}
</Button>
<Button style={{marginTop: "10px"}} shape={"round"} type="primary" danger onClick={() => Setting.openLinkSafe(conference.resultUrl)}>
{i18next.t("conference:View Result")}
</Button>
</Space>
}
/>
</div>
);
}
render() {
const conference = this.props.conference;
const selectedTreeItem = this.getSelectedTreeItem(this.state.selectedKey, conference.treeItems);
if (!selectedTreeItem) {
return null;
}
if (!Setting.isMobile()) {
return (
<div>
<Row>
<Col span={24} >
{
this.renderCompetition(conference)
}
</Col>
</Row>
<Row>
<Col span={4} >
{
this.renderMenu(conference.treeItems)
}
</Col>
<Col span={1} >
</Col>
<Col span={19} >
{
this.renderPage(selectedTreeItem)
}
</Col>
</Row>
</div>
);
} else {
return (
<div>
<Row>
<Row>
<Col span={24} >
{
this.renderCompetition(conference)
}
</Col>
</Row>
<Col span={24} >
{
this.renderMenu(conference.treeItems)
}
</Col>
</Row>
<Row>
<Col span={1} />
<Col span={22} >
{
this.renderPage(selectedTreeItem)
}
</Col>
<Col span={1} />
</Row>
</div>
);
}
}
}
export default withRouter(Conference);

282
web/src/ConferenceEdit.js Normal file
View File

@ -0,0 +1,282 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Empty, Input, Row, Tooltip, Tree} from "antd";
import {DeleteOutlined, PlusOutlined} from "@ant-design/icons";
import * as Setting from "./Setting";
import HtmlEditorBraft from "./HtmlEditorBraft";
import i18next from "i18next";
class ConferenceEdit extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
treePath: "0-0",
expandedKeys: ["0-0", "0-0-0", "0-0-0-0"],
};
}
UNSAFE_componentWillMount() {
}
onDragEnter = info => {
// console.log(info);
// expandedKeys 需要受控时设置
// this.setState({
// expandedKeys: info.expandedKeys,
// });
};
onDrop = info => {
// console.log(info);
const dragIndex = parseInt(info.dragNode.pos.slice(2));
let dropIndex = parseInt(info.node.pos.slice(2));
const dragIndexArray = info.dragNode.pos.slice(2).split("-").map(i => parseInt(i));
const isSecondLevel = info.dropPosition === dropIndex;
const isSecondLevelOld = dragIndexArray.length === 2;
// Setting.showMessage("error", `isSecondLevelOld = ${isSecondLevelOld}, isSecondLevel = ${isSecondLevel}, dragIndex = ${dragIndex}, dropIndex = ${dropIndex}`);
dropIndex = info.dropPosition;
if (dropIndex === -1) {
dropIndex = 0;
} else if (!isSecondLevelOld && dropIndex > dragIndex) {
dropIndex -= 1;
}
// console.log(dragIndex);
// console.log(dropIndex);
let treeItems = this.props.conference.treeItems;
let treeItem = {children: treeItems};
dragIndexArray.forEach(i => {
treeItem = treeItem.children[i];
});
if (dragIndexArray.length === 1) {
treeItems = Setting.deleteRow(treeItems, dragIndex);
} else if (dragIndexArray.length === 2) {
const parentItem = treeItems[dragIndexArray[0]];
parentItem.children = Setting.deleteRow(parentItem.children, dragIndexArray[1]);
}
if (!isSecondLevel) {
treeItems = Setting.insertRow(treeItems, treeItem, dropIndex);
} else {
treeItems[dropIndex].children.push(treeItem);
}
this.props.onUpdateTreeItems(treeItems);
};
updateTree(tree) {
this.props.onUpdateTree(tree);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
getTreeItem(treeItems) {
if (this.state.treePath === null) {
return null;
}
let res = {children: treeItems};
const tokens = this.state.treePath.split("-");
for (let i = 1; i < tokens.length; i++) {
res = res.children[tokens[i]];
}
return res;
}
addTreeItemRow(i) {
let treeItems = this.props.conference.treeItems;
const treeItem = {key: `Title - ${treeItems.length + 1}`, title: `Title - ${treeItems.length + 1}`, titleEn: `Title - ${treeItems.length + 1}`, content: `Content - ${treeItems.length + 1}`, contentEn: `Content - ${treeItems.length + 1}`, children: []};
treeItems = Setting.insertRow(treeItems, treeItem, i);
this.props.onUpdateTreeItems(treeItems);
}
// deleteTreeItemRow(i) {
// let treeItems = this.props.conference.treeItems;
//
// treeItems = Setting.deleteRow(treeItems, i);
// this.props.onUpdateTreeItems(treeItems);
// }
deleteTreeItemRowEx(indexArray) {
let treeItems = this.props.conference.treeItems;
if (indexArray.length === 1) {
treeItems = Setting.deleteRow(treeItems, indexArray[0]);
} else if (indexArray.length === 2) {
const parentItem = treeItems[indexArray[0]];
parentItem.children = Setting.deleteRow(parentItem.children, indexArray[1]);
}
this.props.onUpdateTreeItems(treeItems);
}
renderTree(treeItems) {
// const treeData = this.getTree(treeItems);
const onSelect = (selectedKeys, info) => {
// const i = selectedKeys[0];
const selected = info.selected;
if (!selected) {
this.setState({
treePath: null,
});
return;
}
const treeItem = info.node;
this.setState({
treePath: treeItem.pos, // "0-0"
});
// alert(JSON.stringify(treeItem));
};
const copiedTreeItems = treeItems.map((treeItem, i) => {
const copiedTreeItem = Setting.deepCopy(treeItem);
copiedTreeItem.title = (
<div>
<Button style={{marginRight: "5px"}} icon={<DeleteOutlined />} size="small" onClick={() => this.deleteTreeItemRowEx([i])} />
{
treeItem.title
}
</div>
);
copiedTreeItem.children = copiedTreeItem.children.map((treeItem2, j) => {
const copiedTreeItem2 = Setting.deepCopy(treeItem2);
copiedTreeItem2.title = (
<div>
<Button style={{marginRight: "5px"}} icon={<DeleteOutlined />} size="small" onClick={() => this.deleteTreeItemRowEx([i, j])} />
{
treeItem2.title
}
</div>
);
return copiedTreeItem2;
});
return copiedTreeItem;
});
return (
<div>
<Row style={{marginTop: "10px", marginBottom: "10px"}} >
<Tooltip placement="topLeft" title="Add">
<Button style={{marginRight: "5px"}} icon={<PlusOutlined />} size="small" onClick={() => this.addTreeItemRow(this.props.conference.treeItems.length)} />
</Tooltip>
</Row>
<Tree
className="draggable-tree"
defaultExpandAll={true}
// defaultExpandedKeys={this.state.expandedKeys}
draggable
blockNode
onDragEnter={this.onDragEnter}
onDrop={this.onDrop}
// switcherIcon={<DownOutlined />}
icon={null}
showIcon={true}
defaultSelectedKeys={treeItems.length > 0 ? [treeItems[0].key] : []}
onSelect={onSelect}
treeData={copiedTreeItems}
/>
</div>
);
}
updateTreeItemField(key, value) {
const treeItems = this.props.conference.treeItems;
let treeItem = {children: treeItems};
const tokens = this.state.treePath.split("-");
for (let i = 1; i < tokens.length; i++) {
treeItem = treeItem.children[tokens[i]];
}
treeItem[key] = value;
this.props.onUpdateTreeItems(treeItems);
}
renderPage(treeItem) {
if (treeItem === undefined || treeItem === null) {
return (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
);
}
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Title")}:
</Col>
<Col span={22} >
<Input value={this.props.language !== "en" ? treeItem.title : treeItem.titleEn} onChange={e => {
this.updateTreeItemField("key", e.target.value);
this.updateTreeItemField(this.props.language !== "en" ? "title" : "titleEn", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Content")}:
</Col>
<Col span={22} >
<div style={{height: "600px", border: "1px solid rgb(217,217,217)"}} >
<HtmlEditorBraft key={`${treeItem.key}-${this.props.language}`} text={this.props.language !== "en" ? treeItem.content : treeItem.contentEn} onUpdateText={(text) => {
this.updateTreeItemField(this.props.language !== "en" ? "content" : "contentEn", text);
}} />
</div>
</Col>
</Row>
</div>
);
}
render() {
const conference = this.props.conference;
return (
<Row style={{marginTop: "10px"}} >
<Col span={4} >
{
this.renderTree(conference.treeItems)
}
</Col>
<Col span={1} >
</Col>
<Col span={19} >
{
this.renderPage(this.getTreeItem(conference.treeItems))
}
</Col>
</Row>
);
}
}
export default ConferenceEdit;

View File

@ -0,0 +1,398 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import * as Setting from "./Setting";
import moment from "moment";
import Conference from "./Conference";
import ConferenceEdit from "./ConferenceEdit";
import i18next from "i18next";
const {Option} = Select;
class ConferenceEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
conferenceName: props.match.params.conferenceName,
conference: null,
};
}
UNSAFE_componentWillMount() {
this.getConference();
}
getConference() {
ConferenceBackend.getConference(this.props.account.name, this.state.conferenceName)
.then((conference) => {
this.setState({
conference: conference,
});
});
}
parseConferenceField(key, value) {
if (["score"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateConferenceField(key, value) {
value = this.parseConferenceField(key, value);
const conference = this.state.conference;
conference[key] = value;
this.setState({
conference: conference,
});
}
renderConference() {
return (
<Card size="small" title={
<div>
{i18next.t("conference:Edit Conference")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={this.submitConferenceEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Name")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.name} onChange={e => {
this.updateConferenceField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Display name")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.displayName} onChange={e => {
this.updateConferenceField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Type")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.conference.type} onChange={(value => {this.updateConferenceField("type", value);})}>
{
[
{id: "Conference", name: "Conference"},
{id: "Competition", name: "Competition"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Introduction")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.introduction} onChange={e => {
this.updateConferenceField("introduction", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Start date")}:
</Col>
<Col span={5} >
<DatePicker defaultValue={moment(this.state.conference.startDate, "YYYY-MM-DD")} onChange={(time, timeString) => {
this.updateConferenceField("startDate", timeString);
}} />
</Col>
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:End date")}:
</Col>
<Col span={10} >
<DatePicker defaultValue={moment(this.state.conference.endDate, "YYYY-MM-DD")} onChange={(time, timeString) => {
this.updateConferenceField("endDate", timeString);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Organizer")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.organizer} onChange={e => {
this.updateConferenceField("organizer", e.target.value);
}} />
</Col>
</Row>
{
this.state.conference.type !== "Conference" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Location")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.location} onChange={e => {
this.updateConferenceField("location", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Address")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.address} onChange={e => {
this.updateConferenceField("address", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Carousels")}:
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} placeholder="Please input"
value={this.state.conference.carousels}
onChange={value => {
this.updateConferenceField("carousels", value);
}}
>
{
this.state.conference.carousels.map((carousel, index) => <Option key={carousel}>{carousel}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Carousel height")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.carouselHeight} onChange={e => {
this.updateConferenceField("carouselHeight", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Enable submission")}:
</Col>
<Col span={1} >
<Switch checked={this.state.conference.enableSubmission} onChange={checked => {
this.updateConferenceField("enableSubmission", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Tags")}:
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} placeholder="Please input"
value={this.state.conference.tags}
onChange={value => {
this.updateConferenceField("tags", value);
}}
/>
</Col>
</Row>
{
this.state.conference.type !== "Competition" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Dataset URL")}:
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.conference.datasetUrl} onChange={e => {
this.updateConferenceField("datasetUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Preview URL")}:
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.conference.datasetPreviewUrl} onChange={e => {
this.updateConferenceField("datasetPreviewUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Result URL")}:
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.conference.resultUrl} onChange={e => {
this.updateConferenceField("resultUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Bonus")}:
</Col>
<Col span={22} >
<InputNumber min={0} value={this.state.conference.bonus} onChange={value => {
this.updateConferenceField("bonus", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Person count")}:
</Col>
<Col span={22} >
<InputNumber min={0} value={this.state.conference.personCount} onChange={value => {
this.updateConferenceField("personCount", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Display state")}:
</Col>
<Col span={22} >
<Input value={this.state.conference.displayState} onChange={e => {
this.updateConferenceField("displayState", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Status")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.conference.status} onChange={(value => {this.updateConferenceField("status", value);})}>
{
[
{id: "Public", name: "Public (Everyone can see it)"},
{id: "Hidden", name: "Hidden (Only yourself can see it)"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Default item")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.conference.defaultItem} onChange={value => {this.updateConferenceField("defaultItem", value);}}>
{
this.state.conference.treeItems.filter(treeItem => treeItem.children.length === 0).map((treeItem, index) => <Option key={treeItem.titleEn}>{`${treeItem.title} | ${treeItem.titleEn}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Language")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.conference.language} onChange={(value => {this.updateConferenceField("language", value);})}>
{
[
{id: "zh", name: "zh"},
{id: "en", name: "en"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("conference:Menu")}:
</Col>
<Col span={22} >
<ConferenceEdit conference={this.state.conference} language={this.state.conference.language} onUpdateTreeItems={(value) => {this.updateConferenceField("treeItems", value);}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
<Conference conference={this.state.conference} language={this.state.conference.language} />
</Col>
</Row>
</Card>
);
}
submitConferenceEdit() {
const conference = Setting.deepCopy(this.state.conference);
ConferenceBackend.updateConference(this.state.conference.owner, this.state.conferenceName, conference)
.then((res) => {
if (res) {
Setting.showMessage("success", "Successfully saved");
this.setState({
conferenceName: this.state.conference.name,
});
this.props.history.push(`/conferences/${this.state.conference.name}`);
} else {
Setting.showMessage("error", "failed to save: server side failure");
this.updateConferenceField("name", this.state.conferenceName);
}
})
.catch(error => {
Setting.showMessage("error", `failed to save: ${error}`);
});
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.state.conference !== null ? this.renderConference() : null
}
</Col>
<Col span={1}>
</Col>
</Row>
<Row style={{margin: 10}}>
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large" onClick={this.submitConferenceEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>
);
}
}
export default ConferenceEditPage;

View File

@ -0,0 +1,228 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, Popconfirm, Row, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import i18next from "i18next";
class ConferenceListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
conferences: null,
};
}
UNSAFE_componentWillMount() {
this.getConferences();
}
getConferences() {
ConferenceBackend.getConferences(this.props.account.name)
.then((res) => {
this.setState({
conferences: res,
});
});
}
newConference() {
return {
owner: this.props.account.name,
name: `conference_${this.state.conferences.length}`,
createdTime: moment().format(),
displayName: `New Conference - ${this.state.conferences.length}`,
type: "Conference",
introduction: "Introduction",
startDate: moment().format("YYYY-MM-DD"),
endDate: moment().format("YYYY-MM-DD"),
organizer: "Organizer",
carousels: [],
carouselHeight: "100%",
tags: [],
bonus: 0,
personCount: 0,
displayState: "Started",
status: "Public",
language: "zh",
location: "Shanghai, China",
address: "3663 Zhongshan Road North",
defaultItem: "Home",
treeItems: [{key: "Home", title: "首页", titleEn: "Home", content: "内容", contentEn: "Content", children: []}],
};
}
addConference() {
const newConference = this.newConference();
ConferenceBackend.addConference(newConference)
.then((res) => {
Setting.showMessage("success", "Conference added successfully");
this.setState({
conferences: Setting.prependRow(this.state.conferences, newConference),
});
}
)
.catch(error => {
Setting.showMessage("error", `Conference failed to add: ${error}`);
});
}
deleteConference(i) {
ConferenceBackend.deleteConference(this.state.conferences[i])
.then((res) => {
Setting.showMessage("success", "Conference deleted successfully");
this.setState({
conferences: Setting.deleteRow(this.state.conferences, i),
});
}
)
.catch(error => {
Setting.showMessage("error", `Conference failed to delete: ${error}`);
});
}
renderTable(conferences) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record, index) => {
return (
<Link to={`/conferences/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "300px",
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
},
{
title: i18next.t("conference:Start date"),
dataIndex: "startDate",
key: "startDate",
width: "120px",
sorter: (a, b) => a.startDate.localeCompare(b.startDate),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("conference:End date"),
dataIndex: "endDate",
key: "endDate",
width: "120px",
sorter: (a, b) => a.endDate.localeCompare(b.endDate),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("conference:Organizer"),
dataIndex: "organizer",
key: "organizer",
// width: '120px',
sorter: (a, b) => a.organizer.localeCompare(b.organizer),
},
{
title: i18next.t("conference:Location"),
dataIndex: "location",
key: "location",
width: "140px",
sorter: (a, b) => a.location.localeCompare(b.location),
},
{
title: i18next.t("conference:Address"),
dataIndex: "address",
key: "address",
width: "140px",
sorter: (a, b) => a.address.localeCompare(b.address),
},
{
title: i18next.t("general:Status"),
dataIndex: "status",
key: "status",
width: "80px",
sorter: (a, b) => a.status.localeCompare(b.status),
},
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "170px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/conferences/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete conference: ${record.name} ?`}
onConfirm={() => this.deleteConference(index)}
okText="OK"
cancelText="Cancel"
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
},
},
];
return (
<div>
<Table columns={columns} dataSource={conferences} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{i18next.t("general:Conferences")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addConference.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={conferences === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.conferences)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default ConferenceListPage;

120
web/src/ContactPage.js Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Row} from "antd";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import * as Setting from "./Setting";
import * as Conf from "./Conf";
import i18next from "i18next";
class ContactPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
conference: null,
};
}
UNSAFE_componentWillMount() {
this.getConference();
}
getConference() {
ConferenceBackend.getConference(Conf.DefaultOwner, Conf.DefaultConferenceName)
.then((conference) => {
this.setState({
conference: conference,
});
});
}
renderComments() {
if (this.props.account === undefined) {
return null;
}
const nodeId = "verification";
// const title = encodeURIComponent(document.title);
const title = encodeURIComponent(`Change Tag Request - ${this.props.account.name}`);
const author = this.props.account.name;
const urlPath = encodeURIComponent(`|change-tag-request|${this.props.account.name}`);
let accessToken;
if (this.props.account === null) {
// Confita is signed out, also sign out Casnode.
accessToken = "signout";
} else {
accessToken = this.props.account.accessToken;
}
return (
<iframe
key={accessToken}
title={title}
style={{border: "1px solid rgb(232,232,232)", width: "100%", height: "70vh"}}
src={`${Conf.CasnodeEndpoint}/embedded-replies?nodeId=${nodeId}&title=${title}&author=${author}&urlPath=${urlPath}&accessToken=${accessToken}`}
/>
);
}
renderAdmin() {
let accessToken;
if (this.props.account === null) {
// Confita is signed out, also sign out Casnode.
accessToken = "signout";
} else {
accessToken = this.props.account.accessToken;
}
return (
<iframe
key={accessToken}
title={"Casnode"}
style={{border: "1px solid rgb(232,232,232)", width: "100%", height: "70vh"}}
src={`${Conf.CasnodeEndpoint}?accessToken=${accessToken}`}
/>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={!Setting.isMobile() ? 3 : 0}>
</Col>
<Col span={!Setting.isMobile() ? 18 : 24}>
<br />
{
Setting.getAlert("info", i18next.t("contact:If you want to change your tag to 'Student' or 'Accompanying Person', please contact the service desk in the below chat box. Provide the tag you want to change to and also the proof image like your student ID card photo. You can use the keystroke 'Ctrl' + 'V' to paste an image into the reply box."))
}
<br />
{
(Setting.isEditorUser(this.props.account) || Setting.isAdminUser(this.props.account)) ? (
this.renderAdmin()
) : (
this.renderComments()
)
}
</Col>
<Col span={!Setting.isMobile() ? 3 : 0}>
</Col>
</Row>
</div>
);
}
}
export default ContactPage;

63
web/src/CsvTable.js Normal file
View File

@ -0,0 +1,63 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {readString} from "react-papaparse";
import ReactDataSheet from "react-datasheet";
import "react-datasheet/lib/react-datasheet.css";
class CsvTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
grid: null,
};
}
render() {
if (this.props.conference === null) {
return null;
}
if (this.state.grid === null) {
const csvString = this.props.conference.previewData;
readString(csvString, {
worker: true,
complete: (results) => {
this.setState({
grid: results.data.map((row, i) => {
return row.map(cell => {
return {value: cell, readOnly: i === 0};
});
}),
});
// console.log(results);
},
});
return null;
}
return (
<div>
<ReactDataSheet
data={this.state.grid}
valueRenderer={cell => cell.value}
/>
</div>
);
}
}
export default CsvTable;

142
web/src/HomePage.js Normal file
View File

@ -0,0 +1,142 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Carousel, Col, Row} from "antd";
import Conference from "./Conference";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import * as Setting from "./Setting";
import * as Conf from "./Conf";
import {Helmet} from "react-helmet";
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
conference: null,
isMatchConferenceTreeItem: true,
language: Setting.getLanguage(),
};
}
UNSAFE_componentWillMount() {
this.getConference();
Setting.onLanguageChange(language => {
this.setState({
language,
});
});
}
getConference() {
ConferenceBackend.getConference(Conf.DefaultOwner, Conf.DefaultConferenceName)
.then((conference) => {
this.setState({
conference: conference,
});
});
}
renderCarousel(conference) {
const contentStyle = {
// height: '150px',
color: "#fff",
lineHeight: "160px",
textAlign: "center",
background: "#364d79",
};
return (
<Carousel autoplay>
{
conference.carousels.map((carousel, i) => {
return (
<div key={i}>
<h3 style={contentStyle}>
<img alt={`carousel-${i}`} style={{width: "100%", height: conference.carouselHeight === "" ? "100%" : conference.carouselHeight}} src={carousel} />
</h3>
</div>
);
})
}
</Carousel>
);
}
handleMatchConferenceTreeItem(isMatched) {
this.setState({
isMatchConferenceTreeItem: isMatched,
});
}
renderHome() {
if (this.state.conference === null) {
return null;
}
return (
<div>
<Helmet>
<title>{this.state.language !== "en" ? Conf.title : Conf.titleEn}</title>
<meta
name="description"
content={this.state.language !== "en" ? Conf.title : Conf.titleEn}
/>
<meta
name="keywords"
content={this.state.language !== "en" ? Conf.title : Conf.titleEn}
/>
</Helmet>
<div style={{marginBottom: "-8px"}}>
{
this.renderCarousel(this.state.conference)
}
</div>
<Conference
conference={this.state.conference}
language={this.state.language}
history={this.props.history}
path="/"
enableMenuPath={true}
onMatchTreeItem={this.handleMatchConferenceTreeItem.bind(this)}
/>
</div>
);
}
render() {
if (!this.state.isMatchConferenceTreeItem) {
return null;
}
return (
<div>
<Row style={{width: "100%"}}>
<Col span={!Setting.isMobile() ? 3 : 0}>
</Col>
<Col span={!Setting.isMobile() ? 18 : 24}>
{
this.renderHome()
}
</Col>
<Col span={!Setting.isMobile() ? 3 : 0}>
</Col>
</Row>
</div>
);
}
}
export default HomePage;

View File

@ -0,0 +1,93 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import "braft-editor/dist/index.css";
import React from "react";
import BraftEditor from "braft-editor";
import i18next from "i18next";
const fontFamilies = [
{name: "宋体", family: "SimSun"},
{name: "黑体", family: "SimHei"},
{name: "微软雅黑", family: "Microsoft YaHei , Helvetica, sans-serif"},
{name: "楷体", family: "KaiTi"},
{name: "仿宋", family: "FangSong"},
{name: "Arial", family: "Arial, Helvetica, sans-serif"},
{name: "Times New Roman", family: "Times-New-Roman"},
{name: "Georgia", family: "Georgia, serif"},
{name: "Impact", family: "Impact, serif"},
{name: "Monospace", family: "\"Courier New\", Courier, monospace"},
{name: "Tahoma", family: "tahoma, arial, 'Hiragino Sans GB', 宋体, sans-serif"},
];
class HtmlEditorBraft extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
// font-family style lost:
// https://github.com/margox/braft-editor/issues/775
editorState: BraftEditor.createEditorState(this.props.text, {fontFamilies: fontFamilies}),
};
}
updateText(text) {
this.props.onUpdateText(text);
}
handleChange = (editorState) => {
this.setState({
editorState: editorState,
});
let text = editorState.toHTML();
if (text.startsWith("<p>") && text.endsWith("</p>")) {
text = text.slice(3, -4);
}
this.updateText(text);
};
render() {
const controls = [
"undo", "redo", "separator",
"remove-styles", "hr", "separator",
"bold", "italic", "underline", "strike-through", "superscript", "subscript", "separator",
"headings", "blockquote", "code", "list_ul", "list_ol", "separator",
"link", "text-color", "line-height", "letter-spacing", "text-indent", "separator",
"font-size", "font-family", "text-align", "separator",
"media", "emoji", "clear", "fullscreen",
];
return (
<div>
<BraftEditor
controls={controls}
fontFamilies={fontFamilies}
letterSpacings={[0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 18, 20, 30, 40, 50]}
value={this.state.editorState}
media={{
validateFn: (file) => {
alert(i18next.t("conference:Local image upload is disallowed. Please go to \"Resources\" to upload images and paste the image URL to the text editor. You can choose \"Media\" -> left-bottom corner \"+Add network resource\", then paste the image URL into the box"));
return false;
},
}}
onChange={this.handleChange}
/>
</div>
);
}
}
export default HtmlEditorBraft;

291
web/src/ParticipantTable.js Normal file
View File

@ -0,0 +1,291 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Table, Tooltip} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as UserBackend from "./backend/UserBackend";
import copy from "copy-to-clipboard";
import moment from "moment";
import QrCode from "./QrCode";
const {Option} = Select;
class ParticipantTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
users: [],
};
}
UNSAFE_componentWillMount() {
this.getUsers();
}
getUsers() {
UserBackend.getUsers()
.then((res) => {
this.setState({
users: res,
});
});
}
updateTable(table) {
this.props.onUpdateTable(table);
}
parseField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateField(table, index, key, value) {
value = this.parseField(key, value);
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {name: `Please select a user - ${table.length}`, createdTime: moment().format(), displayName: "", affiliation: "", email: "", tag: "", role: "Panelist", joinUrl: ""};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const userMap = [];
table.forEach(row => {
userMap[row.name] = 1;
});
const columns = [
{
title: i18next.t("room:No."),
dataIndex: "no",
key: "no",
width: "60px",
render: (text, record, index) => {
return (index + 1);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "250px",
render: (text, record, index) => {
return (
<Select virtual={false} showSearch optionFilterProp="label" style={{width: "100%"}} value={`${record.displayName} (${record.name})`} placeholder="Please select user" onChange={name => {
const user = this.state.users.filter(user => `${user.displayName} (${user.name})` === name)[0];
if (user !== undefined) {
this.updateField(table, index, "name", user.name);
this.updateField(table, index, "displayName", user.displayName);
this.updateField(table, index, "email", user.email);
this.updateField(table, index, "affiliation", user.affiliation);
this.updateField(table, index, "tag", user.tag);
if (user.name === "admin") {
this.updateField(table, index, "role", "Host");
}
}
}}
filterOption={(input, option) =>
option.key.indexOf(input) >= 0
}
>
{
this.state.users.filter(user => (userMap[user.name] === undefined)).map((user, index) => <Option key={`${user.displayName} (${user.name})`}>{`${user.displayName} (${user.name})`}</Option>)
}
</Select>
);
// return `${record.displayName} (${record.name})`;
},
},
{
title: i18next.t("payment:Affiliation"),
dataIndex: "affiliation",
key: "affiliation",
// width: '250px',
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Email"),
dataIndex: "email",
key: "email",
width: "200px",
},
{
title: i18next.t("payment:Tag"),
dataIndex: "tag",
key: "tag",
width: "100px",
},
{
title: i18next.t("room:Role"),
dataIndex: "role",
key: "role",
width: "100px",
render: (text, record, index) => {
// https://support.zoom.us/hc/en-us/articles/360040324512-Roles-in-a-meeting
return (
<Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {
this.updateField(table, index, "role", value);
})}>
{
[
{id: "Host", name: "Host"},
// {id: "Co-host", name: "Co-host"},
{id: "Panelist", name: "Panelist"},
{id: "Attendee", name: "Attendee"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
);
},
},
{
title: i18next.t("room:Join In"),
dataIndex: "joinUrl",
key: "joinUrl",
width: "330px",
render: (text, record, index) => {
const startUrl = this.props.room.startUrl;
if (record.name === "admin") {
return (
<div>
<a target="_blank" rel="noreferrer" href={startUrl}>
<Button disabled={startUrl === ""} style={{marginRight: "5px"}} type="primary" danger size="small">{i18next.t("room:Join In")}</Button>
</a>
<Button disabled={startUrl === ""} style={{marginRight: "5px"}} danger size="small" onClick={() => {
copy(startUrl);
Setting.showMessage("success", "Meeting link copied to clipboard successfully");
}}>{i18next.t("room:Copy Meeting Link")}</Button>
{
(text === "") ? (
<Button disabled={text === ""} style={{marginRight: "5px"}} danger size="small">{i18next.t("room:Scan QR Code")}</Button>
) : (
<Tooltip placement="topLeft" color={"white"} overlayStyle={{maxWidth: "1000px"}} title={<QrCode url={startUrl} />}>
<Button disabled={text === ""} style={{marginRight: "5px"}} danger size="small">{i18next.t("room:Scan QR Code")}</Button>
</Tooltip>
)
}
</div>
);
} else {
return (
<div>
<a target="_blank" rel="noreferrer" href={text}>
<Button disabled={text === ""} style={{marginRight: "5px"}} type="primary" size="small">{i18next.t("room:Join In")}</Button>
</a>
<Button disabled={text === ""} style={{marginRight: "5px"}} size="small" onClick={() => {
copy(text);
Setting.showMessage("success", "Meeting link copied to clipboard successfully");
}}>{i18next.t("room:Copy Meeting Link")}</Button>
{
(text === "") ? (
<Button disabled={text === ""} style={{marginRight: "5px"}} size="small">{i18next.t("room:Scan QR Code")}</Button>
) : (
<Tooltip placement="topLeft" color={"white"} overlayStyle={{maxWidth: "1000px"}} title={<QrCode url={text} />}>
<Button disabled={text === ""} style={{marginRight: "5px"}} size="small">{i18next.t("room:Scan QR Code")}</Button>
</Tooltip>
)
}
</div>
);
}
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={"Up"}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Down"}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Delete"}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default ParticipantTable;

186
web/src/PaymentCard.js Normal file
View File

@ -0,0 +1,186 @@
// Copyright 2022 The Casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Alert, Button, Card, Col, Space} from "antd";
import {DownloadOutlined} from "@ant-design/icons";
import * as Setting from "./Setting";
import {withRouter} from "react-router-dom";
import i18next from "i18next";
import * as Conf from "./Conf";
const {Meta} = Card;
class PaymentCard extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
renderPayment(product, payment) {
if (product.name !== payment.productName) {
return null;
}
return (
<Alert
message={`${Setting.getState(payment)} | ${Setting.getPrice(payment)}`}
showIcon
description={
<div>
{`${i18next.t("general:Name")}: ${payment.name}`}
<br />
{`${i18next.t("general:Created time")}: ${Setting.getFormattedDate(payment.createdTime)}`}
<br />
<Button style={{marginTop: "5px"}} type="primary" shape="round" icon={<DownloadOutlined />} onClick={(e) => {
e.stopPropagation();
if (payment.invoiceUrl === "") {
Setting.goToLink(Setting.getPaymentInvoiceUrl(this.props.account, payment));
} else {
Setting.openLinkSafe(payment.invoiceUrl);
}
}}>
{payment.invoiceUrl === "" ? i18next.t("payment:Issue Invoice") :
i18next.t("payment:Download Invoice")}
</Button>
</div>
}
type="success"
style={{cursor: "pointer"}}
onClick={() => {
Setting.goToLink(Setting.getPaymentUrl(this.props.account, payment));
}}
action={
<Space direction="vertical">
{
`${payment.type}`
}
</Space>
}
/>
);
}
updateProduct(type) {
this.props.onUpdateProduct(type, this.props.product);
}
onClickCard(link, clickable) {
if (!clickable) {
return;
}
if (Conf.TestAffiliation !== "") {
if ((!Setting.isEditorUser(this.props.account) && !Setting.isAdminUser(this.props.account))) {
if (!this.props.account.affiliation.includes(Conf.TestAffiliation)) {
this.updateProduct("test");
return;
}
}
}
if (!this.isRightProduct()) {
this.updateProduct("product");
return;
}
Setting.goToLink(link);
}
renderCardMobile(logo, link, title, desc, time, isSingle, clickable) {
if (title.includes("-1")) {
return null;
}
const cursor = clickable ? "pointer" : "auto";
const opacity = this.isRightProduct() ? 1.0 : 0.8;
const backgroundColor = this.isRightProduct() ? null : "rgb(255,242,240)";
const gridStyle = {
width: "100vw",
textAlign: "center",
cursor: cursor,
opacity: opacity,
backgroundColor: backgroundColor,
};
return (
<Card.Grid style={gridStyle} onClick={() => {
this.onClickCard(link, clickable);
}}>
<img src={logo} alt="logo" height={60} style={{marginBottom: "20px"}} />
<Meta title={title} description={desc} />
</Card.Grid>
);
}
isRightProduct() {
if (Setting.isEditorUser(this.props.account) || Setting.isAdminUser(this.props.account)) {
return true;
}
return this.props.account.tag === this.props.product.tag;
}
renderCard(logo, link, title, desc, time, isSingle, clickable) {
if (title.includes("-1")) {
return (
<Col style={{paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px"}} span={7} />
);
}
const cursor = clickable ? "pointer" : "auto";
const opacity = this.isRightProduct() ? 1.0 : 0.8;
const backgroundColor = this.isRightProduct() ? null : "rgb(255,242,240)";
const bodyStyle = this.isRightProduct() ? {} : {backgroundSize: "100% 100%", backgroundImage: "url(https://cdn.casbin.com/static/img/mark2.png)"};
return (
<Col style={{opacity: opacity, paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px"}} span={7}>
<Card
hoverable
cover={
<img alt="logo" src={logo} width={"100%"} height={"100%"} />
}
onClick={() => {
this.onClickCard(link, clickable);
}}
bodyStyle={bodyStyle}
style={isSingle ? {width: "320px", cursor: cursor, backgroundColor: backgroundColor} : {cursor: cursor, backgroundColor: backgroundColor}}
>
<Meta title={<h1>{title}</h1>} description={<h2>{Setting.getLanguageText(desc)}</h2>} />
<br />
<br />
{
this.props.payments.map(payment => {
return this.renderPayment(this.props.product, payment);
})
}
{/* <Meta title={""} description={Setting.getFormattedDateShort(time)} />*/}
</Card>
</Col>
);
}
render() {
if (Setting.isMobile()) {
return this.renderCardMobile(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.isSingle, this.props.clickable);
} else {
return this.renderCard(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.isSingle, this.props.clickable);
}
}
}
export default withRouter(PaymentCard);

260
web/src/PaymentListPage.js Normal file
View File

@ -0,0 +1,260 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Row, Table} from "antd";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";
import i18next from "i18next";
import XLSX from "xlsx";
import FileSaver from "file-saver";
import moment from "moment";
class PaymentListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
payments: null,
};
}
UNSAFE_componentWillMount() {
this.getGlobalPayments();
}
getGlobalPayments() {
PaymentBackend.getGlobalPayments()
.then((res) => {
this.setState({
payments: res,
});
});
}
downloadXlsx() {
const data = [];
this.state.payments.forEach((payment, i) => {
if (payment.state !== "Paid") {
return;
}
const row = {};
row["User"] = payment.user;
row["Payment ID"] = payment.name;
row["Created time"] = Setting.getFormattedDate(payment.createdTime);
row["Product"] = payment.productDisplayName.split("|")[1];
row["Detail"] = payment.detail;
row["Tag"] = payment.tag;
row["Price"] = payment.price;
row["Currency"] = payment.currency;
row["State"] = payment.state;
row["Invoice person name"] = payment.personName;
row["Invoice person ID card"] = payment.personIdCard;
row["Invoice person Email"] = payment.personEmail;
row["Invoice type"] = payment.invoiceType;
row["Invoice title"] = payment.invoiceTitle;
row["Invoice tax ID"] = payment.invoiceTaxId;
row["Invoice remark"] = payment.invoiceRemark;
data.push(row);
});
const sheet = XLSX.utils.json_to_sheet(data);
sheet["!cols"] = [
{wch: 20},
{wch: 25},
{wch: 20},
{wch: 30},
{wch: 25},
{wch: 10},
{wch: 10},
{wch: 10},
{wch: 10},
{wch: 20},
{wch: 20},
{wch: 20},
{wch: 20},
{wch: 40},
{wch: 20},
{wch: 150},
];
const blob = Setting.sheet2blob(sheet, "Default");
const fileName = `payments-${Setting.getFormattedDate(moment().format())}.xlsx`;
FileSaver.saveAs(blob, fileName);
}
renderTable(payments) {
const columns = [
// {
// title: i18next.t("general:Organization"),
// dataIndex: 'organization',
// key: 'organization',
// width: '120px',
// sorter: (a, b) => a.organization.localeCompare(b.organization),
// render: (text, record, index) => {
// return (
// <Link to={`/organizations/${text}`}>
// {text}
// </Link>
// )
// }
// },
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "120px",
sorter: (a, b) => a.user.localeCompare(b.user),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={Setting.getUserProfileUrl(text)}>
{text}
</a>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={Setting.getPaymentInvoiceUrl(this.props.account, record)}>
{text}
</a>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("payment:Product"),
dataIndex: "productDisplayName",
key: "productDisplayName",
// width: '160px',
sorter: (a, b) => a.productDisplayName.localeCompare(b.productDisplayName),
render: (text, record, index) => {
return Setting.getLanguageText(text);
},
},
{
title: i18next.t("payment:Detail"),
dataIndex: "detail",
key: "detail",
// width: '160px',
sorter: (a, b) => a.detail.localeCompare(b.detail),
},
{
title: i18next.t("payment:Tag"),
dataIndex: "tag",
key: "tag",
width: "100px",
sorter: (a, b) => a.tag.localeCompare(b.tag),
render: (text, record, index) => {
return Setting.getLanguageText(text);
},
},
{
title: i18next.t("payment:Price"),
dataIndex: "price",
key: "price",
width: "120px",
sorter: (a, b) => a.price.localeCompare(b.price),
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: (a, b) => a.currency.localeCompare(b.currency),
render: (text, record, index) => {
return Setting.getCurrencyText(record);
},
},
{
title: i18next.t("payment:State"),
dataIndex: "state",
key: "state",
width: "120px",
sorter: (a, b) => a.state.localeCompare(b.state),
render: (text, record, index) => {
return Setting.getState(record);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "260px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => Setting.openLinkSafe(Setting.getPaymentUrl(this.props.account, record))}>{i18next.t("payment:View Result")}</Button>
<Button disabled={record.state !== "Paid"} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => Setting.openLinkSafe(Setting.getPaymentInvoiceUrl(this.props.account, record))}>{i18next.t("payment:View Invoice")}</Button>
</div>
);
},
},
];
return (
<div>
<Table columns={columns} dataSource={payments} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{i18next.t("general:All Payments")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={() => this.downloadXlsx()}>{i18next.t("general:Download")} (.xlsx)</Button>
</div>
)}
loading={payments === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.payments)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default PaymentListPage;

565
web/src/PaymentPage.js Normal file
View File

@ -0,0 +1,565 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Descriptions, List, Modal, Row, Tooltip} from "antd";
import {CloseCircleTwoTone} from "@ant-design/icons";
import * as SubmissionBackend from "./backend/SubmissionBackend";
import * as ProductBackend from "./backend/ProductBackend";
import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {Link} from "react-router-dom";
import {FilePdfOutlined, FileWordOutlined} from "@ant-design/icons";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import * as Conf from "./Conf";
import PaymentCard from "./PaymentCard";
import Hotkeys from "react-hot-keys";
class PaymentPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
submissions: null,
products: null,
payments: null,
conference: null,
isModalVisible: false,
isTestModalVisible: false,
currentProduct: null,
isEarlyVisible: false,
};
}
UNSAFE_componentWillMount() {
this.getSubmissions();
this.getProducts();
this.getPayments();
this.getConference();
}
getSubmissions() {
SubmissionBackend.getSubmissions(this.props.account.name)
.then((res) => {
this.setState({
submissions: res,
});
});
}
getProducts() {
ProductBackend.getProducts()
.then((res) => {
this.setState({
products: res,
});
});
}
getPayments() {
PaymentBackend.getPayments(this.props.account.name)
.then((payments) => {
this.setState({
payments: payments.filter(payment => payment.state === "Paid"),
});
});
}
getConference() {
ConferenceBackend.getConference(Conf.DefaultOwner, Conf.DefaultConferenceName)
.then((conference) => {
this.setState({
conference: conference,
});
});
}
getDisplayTag(inputTag) {
let myTag = "";
this.state.conference?.tags?.forEach((tag, index) => {
const tokens = tag.split("|");
if (tokens[0] === inputTag) {
if (Setting.getLanguage() !== "zh") {
myTag = tokens[0];
} else {
myTag = tokens[1];
}
}
});
if (inputTag === "Editor") {
if (Setting.getLanguage() !== "zh") {
myTag = "Editor";
} else {
myTag = "编辑";
}
}
return myTag;
}
renderSubmissionList() {
if (this.state.submissions === null) {
return null;
}
return (
<List
itemLayout="horizontal"
dataSource={this.state.submissions}
renderItem={submission => (
<List.Item
actions={[
<Link to={"/submissions"} key="view">
{i18next.t("payment:View")}
</Link>,
<Link to={`/submissions/${submission.owner}/${submission.name}`} key="edit">
{i18next.t("payment:Edit")}
</Link>,
]}
>
<List.Item.Meta
// avatar={<Avatar src={item.picture.large} />}
title={
<Link to={"/submissions"}>
{submission.title}
</Link>
}
description={submission.authors.map(author => `${author.name} | ${author.affiliation} | ${author.email}`).join(", ")}
/>
<Row style={{width: "40%"}} >
<Col span={12} >
<div>{`${submission.conference} | ${submission.type} | ${submission.subType}`}</div>
</Col>
<Col span={12} >
<div style={{width: "180px"}}>
{
Setting.getAlert(submission.status === "ReadyForReview" ? "success" : "error", submission.status)
}
</div>
</Col>
</Row>
{/* {*/}
{/* submission.absWordFileUrl === "" ? (*/}
{/* <Tooltip title={Setting.getFilenameFromUrl(submission.absWordFileUrl)}>*/}
{/* <Button style={{width: 160, height: 78, margin: '10px', cursor: 'auto'}} type="dashed" >*/}
{/* <div>*/}
{/* <FileWordOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />*/}
{/* </div>*/}
{/* <div>*/}
{/* {i18next.t("general:(empty)")}*/}
{/* </div>*/}
{/* </Button>*/}
{/* </Tooltip>*/}
{/* ) : (*/}
{/* <Tooltip title={Setting.getFilenameFromUrl(submission.absWordFileUrl)}>*/}
{/* <Button style={{width: 160, height: 78, margin: '10px'}} type="dashed" onClick={() => Setting.goToLink(submission.absWordFileUrl)}>*/}
{/* <div>*/}
{/* <FileWordOutlined style={{fontSize: 48, color: "rgb(19,77,178)"}} />*/}
{/* </div>*/}
{/* <div>*/}
{/* {Setting.getShortText(Setting.getFilenameFromUrl(submission.absWordFileUrl), 10)}*/}
{/* </div>*/}
{/* </Button>*/}
{/* </Tooltip>*/}
{/* )*/}
{/* }*/}
{/* {*/}
{/* submission.absPdfFileUrl === "" ? (*/}
{/* <Tooltip title={Setting.getFilenameFromUrl(submission.absPdfFileUrl)}>*/}
{/* <Button style={{width: 160, height: 78, margin: '10px', cursor: 'auto'}} type="dashed" >*/}
{/* <div>*/}
{/* <FilePdfOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />*/}
{/* </div>*/}
{/* <div>*/}
{/* {i18next.t("general:(empty)")}*/}
{/* </div>*/}
{/* </Button>*/}
{/* </Tooltip>*/}
{/* ) : (*/}
{/* <Tooltip title={Setting.getFilenameFromUrl(submission.absPdfFileUrl)}>*/}
{/* <Button style={{width: 160, height: 78, margin: '10px'}} type="dashed" onClick={() => Setting.goToLink(submission.absPdfFileUrl)}>*/}
{/* <div>*/}
{/* <FilePdfOutlined style={{fontSize: 48, color: "rgb(194,10,10)"}} />*/}
{/* </div>*/}
{/* <div>*/}
{/* {Setting.getShortText(Setting.getFilenameFromUrl(submission.absPdfFileUrl), 10)}*/}
{/* </div>*/}
{/* </Button>*/}
{/* </Tooltip>*/}
{/* )*/}
{/* }*/}
{
submission.fullWordFileUrl === "" ? (
<Tooltip title={Setting.getFilenameFromUrl(submission.fullWordFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px", cursor: "auto"}} type="dashed" >
<div>
<FileWordOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />
</div>
<div>
{i18next.t("general:(empty)")}
</div>
</Button>
</Tooltip>
) : (
<Tooltip title={Setting.getFilenameFromUrl(submission.fullWordFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px"}} type="dashed" onClick={() => Setting.goToLink(submission.fullWordFileUrl)}>
<div>
<FileWordOutlined style={{fontSize: 48, color: "rgb(19,77,178)"}} />
</div>
<div>
{Setting.getShortText(Setting.getFilenameFromUrl(submission.fullWordFileUrl), 10)}
</div>
</Button>
</Tooltip>
)
}
{
submission.fullPdfFileUrl === "" ? (
<Tooltip title={Setting.getFilenameFromUrl(submission.fullPdfFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px", cursor: "auto"}} type="dashed" >
<div>
<FilePdfOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />
</div>
<div>
{i18next.t("general:(empty)")}
</div>
</Button>
</Tooltip>
) : (
<Tooltip title={Setting.getFilenameFromUrl(submission.fullPdfFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px"}} type="dashed" onClick={() => Setting.goToLink(submission.fullPdfFileUrl)}>
<div>
<FilePdfOutlined style={{fontSize: 48, color: "rgb(194,10,10)"}} />
</div>
<div>
{Setting.getShortText(Setting.getFilenameFromUrl(submission.fullPdfFileUrl), 10)}
</div>
</Button>
</Tooltip>
)
}
{
submission.finalWordFileUrl === "" ? (
<Tooltip title={Setting.getFilenameFromUrl(submission.finalWordFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px", cursor: "auto"}} type="dashed" >
<div>
<FileWordOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />
</div>
<div>
{i18next.t("general:(empty)")}
</div>
</Button>
</Tooltip>
) : (
<Tooltip title={Setting.getFilenameFromUrl(submission.finalWordFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px"}} type="dashed" onClick={() => Setting.goToLink(submission.finalWordFileUrl)}>
<div>
<FileWordOutlined style={{fontSize: 48, color: "rgb(19,77,178)"}} />
</div>
<div>
{Setting.getShortText(Setting.getFilenameFromUrl(submission.finalWordFileUrl), 10)}
</div>
</Button>
</Tooltip>
)
}
{
submission.finalPdfFileUrl === "" ? (
<Tooltip title={Setting.getFilenameFromUrl(submission.finalPdfFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px", cursor: "auto"}} type="dashed" >
<div>
<FilePdfOutlined style={{fontSize: 48, color: "rgb(164,164,164)"}} />
</div>
<div>
{i18next.t("general:(empty)")}
</div>
</Button>
</Tooltip>
) : (
<Tooltip title={Setting.getFilenameFromUrl(submission.finalPdfFileUrl)}>
<Button style={{width: 160, height: 78, margin: "10px"}} type="dashed" onClick={() => Setting.goToLink(submission.finalPdfFileUrl)}>
<div>
<FilePdfOutlined style={{fontSize: 48, color: "rgb(194,10,10)"}} />
</div>
<div>
{Setting.getShortText(Setting.getFilenameFromUrl(submission.finalPdfFileUrl), 10)}
</div>
</Button>
</Tooltip>
)
}
</List.Item>
)}
/>
);
}
updateProduct(type, product) {
if (type === "product") {
this.setState({
isModalVisible: true,
currentProduct: product,
});
} else if (type === "test") {
this.setState({
isTestModalVisible: true,
});
}
}
renderCard(product, isSingle, payments) {
const url = Setting.getProductBuyUrl(this.props.account, product.name);
const price = Setting.getPrice(product);
const paid = payments.length !== 0;
return (
<PaymentCard logo={product.image} link={url} title={price} desc={product.displayName} time={product.tag} isSingle={isSingle} key={product.name} account={this.props.account} product={product} payments={payments} clickable={!paid} onUpdateProduct={(type, product) => {this.updateProduct(type, product);}} />
);
}
renderCards() {
let products = this.state.products;
if (products === null) {
return null;
}
const payments = this.state.payments;
const isSingle = products.length === 1;
if (!this.state.isEarlyVisible && payments.filter(payment => payment.state === "Paid" && payment.productName.includes("early")).length === 0) {
products = products.filter(product => !product.name.includes("early"));
}
if (Setting.isMobile()) {
return (
<Card bodyStyle={{padding: 0}}>
{
products.map(product => {
return this.renderCard(product, isSingle, payments);
})
}
</Card>
);
} else {
return (
<div style={{marginRight: "15px", marginLeft: "15px"}}>
<Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}>
{
products.map(product => {
return this.renderCard(product, isSingle, payments);
})
}
</Row>
</div>
);
}
}
getPaid() {
return this.state.payments.length !== 0;
}
renderAlert(paid) {
if (Conf.paymentInfo !== "" && Conf.paymentInfoEn !== "") {
return Setting.getAlert("info", Setting.getLanguage() !== "zh" ? Conf.paymentInfoEn : Conf.paymentInfo);
}
if (paid) {
return Setting.getAlert("success", i18next.t("payment:You have completed the payment."));
} else {
return Setting.getAlert("error", i18next.t("payment:You haven't completed the payment, please click the button to pay."));
}
}
renderPaymentList() {
if (this.state.payments === null) {
return null;
}
const ths = this;
const displayTag = this.getDisplayTag(this.props.account.tag);
const paid = this.getPaid();
return (
<div>
<div style={{fontSize: 16}}>
{
Setting.getAlert("warning", <div>
{
`${i18next.t("payment:Your current tag is")}: ${displayTag}. `
}
{
`${i18next.t("payment:If you believe your tag is wrong, please click the button to change it")}: `
}
<Button type="primary" size={"small"} disabled={paid} onClick={() => {
Setting.goToContact(ths);
}} >
{i18next.t("payment:Change My Tag")}
</Button>
</div>)
}
</div>
<div style={{fontSize: 16, marginTop: 12}}>
{
this.renderAlert(paid)
}
</div>
{
this.renderCards(paid)
}
</div>
);
}
renderModal() {
const ths = this;
const handleChangeMyTag = () => {
Setting.goToContact(ths);
};
const handleCancel = () => {
this.setState({
isModalVisible: false,
});
};
return (
<Modal title={
<div>
<CloseCircleTwoTone twoToneColor="rgb(255,77,79)" />
{" " + i18next.t("payment:There is error when processing the registration payment..")}
</div>
}
visible={this.state.isModalVisible}
onOk={handleChangeMyTag}
onCancel={handleCancel}
okText={i18next.t("payment:Change My Tag")}
cancelText={i18next.t("payment:Cancel")}>
<p>
{
i18next.t("payment:Your current tag is") + ": "
}
{
Setting.getTag(this.getDisplayTag(this.props.account.tag))
}
{
", " + i18next.t("payment:but this registration requires the tag to be") + ": "
}
{
Setting.getTag(this.getDisplayTag(this.state.currentProduct?.tag))
}
</p>
<p>
{
i18next.t("payment:If you want to switch to another tag, please click the 'Change My Tag' button as below. If you don't need to change the tag, just click the 'Cancel' button.")
}
</p>
{
(Setting.isEditorUser(this.props.account) || Setting.isAdminUser(this.props.account)) ? (
<Button type="primary" onClick={() => {
Setting.goToLink(Setting.getProductBuyUrl(this.props.account, this.state.currentProduct.name));
}}>
{i18next.t("payment:Still Pay (Editor Only)")}
</Button>
) : null
}
</Modal>
);
}
renderTestModal() {
const handleOk = () => {
this.setState({
isTestModalVisible: false,
});
};
return (
<Modal title={
<div>
<CloseCircleTwoTone twoToneColor="rgb(255,77,79)" />
{" " + i18next.t("payment:There is error when processing the registration payment..")}
</div>
}
visible={this.state.isTestModalVisible}
cancelButtonProps={{
style: {
display: "none",
},
}}
onOk={handleOk}
onCancel={handleOk}
okText={i18next.t("payment:OK")}
>
<p>
{
i18next.t("payment:The payment functionality is not available yet, please wait for the announcement.")
}
</p>
</Modal>
);
}
onKeyUp(keyName, e, handle) {}
onKeyDown(keyName, e, handle) {
this.setState({
isEarlyVisible: true,
});
}
render() {
const account = this.props.account;
return (
<div style={{padding: "20px"}}>
<Hotkeys keyName="ctrl+shift+k" onKeyDown={this.onKeyDown.bind(this)} onKeyUp={this.onKeyUp.bind(this)} />
{
this.renderModal()
}
{
this.renderTestModal()
}
<Descriptions title={`${i18next.t("payment:Welcome")}, ${account?.displayName}`} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={1}>
<img src={account?.avatar} alt={account?.avatar} height={90} />
<span style={{fontSize: 28, marginLeft: "20px"}}>
{account?.displayName}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("payment:Username")}><span style={{fontSize: 16}}>{account?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("payment:ID")}><span style={{fontSize: 16}}>{account?.ranking}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("payment:Affiliation")}><span style={{fontSize: 16}}>{account?.affiliation}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("payment:Title")}><span style={{fontSize: 16}}>{account?.title}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("payment:Tag")}><span style={{fontSize: 16}}>{this.getDisplayTag(this.props.account.tag)}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Conferences")} span={3}><span style={{fontSize: 16}}>{this.state.conference?.displayName}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Submissions")} span={3}>
{
this.renderSubmissionList()
}
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Registrations")} span={3}>
{
this.renderPaymentList()
}
</Descriptions.Item>
</Descriptions>
</div>
);
}
}
export default PaymentPage;

49
web/src/QrCode.js Normal file
View File

@ -0,0 +1,49 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {QRCodeSVG} from "qrcode.react";
import * as Conf from "./Conf";
class QrCode extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
render() {
return (
<QRCodeSVG
value={this.props.url}
size={256}
bgColor={"#ffffff"}
fgColor={"#000000"}
level={"L"}
includeMargin={false}
imageSettings={{
src: Conf.QrCodeImageUrl,
x: null,
y: null,
height: 24,
width: 24,
excavate: true,
}}
/>
);
}
}
export default QrCode;

317
web/src/RoomCard.js Normal file
View File

@ -0,0 +1,317 @@
// Copyright 2022 The Casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {withRouter} from "react-router-dom";
import {Button, Card, Col, Collapse, Popconfirm, Spin, Tag, Tooltip} from "antd";
import {CalendarOutlined, ClockCircleOutlined, SyncOutlined, VideoCameraOutlined} from "@ant-design/icons";
import * as Setting from "./Setting";
import i18next from "i18next";
import QrCode from "./QrCode";
import Slot from "./Slot";
const {Meta} = Card;
const {Panel} = Collapse;
class RoomCard extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
nowTime: new Date(),
};
}
requireRegistering() {
if (this.props.room.meetingNumber === "" || this.props.room.meetingNumber === "123456789") {
return false;
}
if (Setting.isAdminUser(this.props.account)) {
return false;
} else {
if (!Setting.isMeetingUser(this.props.account, this.props.payments)) {
return false;
} else if (this.getJoinUrl() === "") {
return true;
}
return false;
}
}
UNSAFE_componentWillMount() {
if (this.requireRegistering()) {
this.registerRoom(this.props.index);
}
this.setTimer();
}
componentWillUnmount() {
clearInterval(this.interval);
}
setTimer() {
const handler = () => {
this.setState({
nowTime: new Date(),
});
};
this.interval = setInterval(handler, 1000);
}
registerRoom(index) {
this.props.onRegisterRoom(index);
}
getJoinUrl() {
if (this.props.account === null) {
return "(anonymous)";
}
const room = this.props.room;
const participant = room.participants.filter(participant => participant.name === this.props.account.name)[0];
return participant === undefined ? "" : participant.joinUrl;
}
renderButtons(index, room) {
const startUrl = room.startUrl;
const joinUrl = this.getJoinUrl();
if (Setting.isAdminUser(this.props.account)) {
return (
<div style={{textAlign: "center"}}>
<a target="_blank" rel="noreferrer" href={startUrl}>
<Button disabled={startUrl === ""} style={{marginRight: "10px", marginBottom: "10px"}} danger>
{
room.status === "Started" ? i18next.t("room:Join In") :
i18next.t("room:Start Meeting")
}
</Button>
</a>
{
Setting.isMobile() ? null : (
(startUrl === "") ? (
<Button disabled={startUrl === ""} style={{marginRight: "10px", marginBottom: "10px"}} danger>{i18next.t("room:Scan QR Code")}</Button>
) : (
<Tooltip placement="topLeft" color={"white"} overlayStyle={{maxWidth: "1000px"}} title={<QrCode url={startUrl} />}>
<Button disabled={startUrl === ""} style={{marginRight: "10px", marginBottom: "10px"}} danger>{i18next.t("room:Scan QR Code")}</Button>
</Tooltip>
)
)
}
<Button disabled={!room.isLive} icon={<VideoCameraOutlined />} style={{marginRight: "10px", marginBottom: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>
{i18next.t("room:Watch Live")}
{Setting.getRoomLiveUserCount(room)}
</Button>
{/* <Button disabled={room.isLive || room.videoUrl === ""} icon={<PlayCircleOutlined />} style={{marginRight: "10px", marginBottom: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>{i18next.t("room:Watch Playback")}</Button>*/}
<Button style={{marginRight: "10px", marginBottom: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete room: ${room.name} ?`}
onConfirm={() => this.deleteRoom(index)}
okText="OK"
cancelText="Cancel"
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
} else {
return (
<div style={{textAlign: "center"}}>
{
(this.props.account === null || !Setting.isMeetingUser(this.props.account, this.props.payments)) ? null : (
<React.Fragment>
<a target="_blank" rel="noreferrer" href={joinUrl}>
<Button disabled={room.meetingNumber === "" || joinUrl === "" || joinUrl === "(anonymous)" || room.status !== "Started"} style={{marginRight: "10px", marginBottom: "10px"}} type="primary" >{i18next.t("room:Join In")}</Button>
</a>
{
Setting.isMobile() ? null : (
(room.meetingNumber === "" || joinUrl === "" || joinUrl === "(anonymous)" || room.status !== "Started") ? (
<Button disabled={true} style={{marginRight: "10px", marginBottom: "10px"}}>{i18next.t("room:Scan QR Code")}</Button>
) : (
<Tooltip placement="topLeft" color={"white"} overlayStyle={{maxWidth: "1000px"}} title={<QrCode url={joinUrl} />}>
<Button disabled={false} style={{marginRight: "10px", marginBottom: "10px"}}>{i18next.t("room:Scan QR Code")}</Button>
</Tooltip>
)
)
}
</React.Fragment>
)
}
<Button disabled={!room.isLive} icon={<VideoCameraOutlined />} style={{marginRight: "10px", marginBottom: "10px"}} type="primary" danger onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>
{i18next.t("room:Watch Live")}
{Setting.getRoomLiveUserCount(room)}
</Button>
{/* <Button disabled={room.isLive || room.videoUrl === ""} icon={<PlayCircleOutlined />} style={{marginRight: "10px", marginBottom: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>{i18next.t("room:Watch Playback")}</Button>*/}
</div>
);
}
}
renderSlotState(slotState) {
if (slotState === "LIVE") {
return (
<Tag icon={<SyncOutlined spin />} color="error">
{slotState}
</Tag>
);
} else if (slotState === "NEXT") {
return (
<Tag icon={<ClockCircleOutlined />} color="default">
{slotState}
</Tag>
);
} else {
return null;
}
}
renderCardMobile(logo, link, title, desc, time, slotState, isSingle, index, room, showButtons) {
const gridStyle = {
width: "100vw",
textAlign: "center",
cursor: "pointer",
};
return (
<Card.Grid key={room.name} style={gridStyle}>
<img src={logo} alt="logo" height={60} style={{marginBottom: "20px", padding: "0px"}} />
<Meta title={<h3>{Setting.getLanguageText(title)}</h3>} description={
<span style={{fontWeight: "bold", color: "rgb(90,90,90)"}}>
{this.renderSlotState(slotState)}
{desc}
</span>
} />
<br />
<Meta title={""} description={time} />
<br />
{
!showButtons ? null : this.renderButtons(index, room)
}
<div>
<Collapse defaultActiveKey={[]}>
<Panel header={<div>
&nbsp;
&nbsp;
<CalendarOutlined />
&nbsp;
{i18next.t("room:View Agenda")}
</div>} key="agenda">
<Slot account={this.props.account} room={room} slots={room.slots} />
</Panel>
</Collapse>
</div>
</Card.Grid>
);
}
renderCard(logo, link, title, desc, time, slotState, isSingle, index, room, showButtons) {
return (
<Col key={room.name} style={{paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px"}} span={6}>
<Card
hoverable
cover={
<img alt="logo" src={logo} style={{width: "100%", height: "210px", objectFit: "scale-down", padding: "20px"}} />
}
style={isSingle ? {width: "420px", cursor: "default"} : {width: "22vw", cursor: "default"}}
>
<Meta title={<h3>{Setting.getLanguageText(title)}</h3>} description={
<span style={{fontWeight: "bold", color: "rgb(90,90,90)"}}>
{this.renderSlotState(slotState)}
{desc}
</span>
} />
{
time === "" ? null : (
<React.Fragment>
<br />
<Meta title={""} description={time} />
<br />
</React.Fragment>
)
}
<br />
{
!showButtons ? null : this.renderButtons(index, room)
}
<Slot account={this.props.account} room={room} slots={room.slots} />
</Card>
</Col>
);
}
getSlotTimeDiff(slot, key) {
// 2022-05-15T23:04:00+08:00
const slotTime = `${slot.date}T${slot[key]}:00+08:00`;
return new Date(Date.parse(slotTime)) - this.state.nowTime;
}
getTargetSlot(room) {
for (let i = 0; i < room.slots.length; i++) {
const slot = room.slots[i];
const startDiff = this.getSlotTimeDiff(slot, "startTime");
const endDiff = this.getSlotTimeDiff(slot, "endTime");
if (startDiff > 0) {
return [slot, "NEXT"];
} else {
if (endDiff > 0) {
return [slot, "LIVE"];
}
}
}
return [null, ""];
}
renderContent() {
const index = this.props.index;
const room = this.props.room;
const arr = this.getTargetSlot(room);
const slot = arr[0];
const slotState = arr[1];
let desc = this.props.desc;
let time = this.props.time;
if (slot !== null) {
desc = slot.title;
time = slot.speaker;
}
// eslint-disable-next-line no-console
console.log(time);
let showButtons = true;
if (Setting.isBranchUser(this.props.account)) {
showButtons = slot?.type === "Plenary";
}
if (Setting.isMobile()) {
return this.renderCardMobile(this.props.logo, this.props.link, this.props.title, desc, time, slotState, this.props.isSingle, index, room, showButtons);
} else {
return this.renderCard(this.props.logo, this.props.link, this.props.title, desc, time, slotState, this.props.isSingle, index, room, showButtons);
}
}
render() {
return (
<Spin key={this.props.room.name} spinning={this.requireRegistering()} size="large" tip={i18next.t("room:Registering...")} style={{paddingTop: "10%"}} >
{
this.renderContent()
}
</Spin>
);
}
}
export default withRouter(RoomCard);

447
web/src/RoomEditPage.js Normal file
View File

@ -0,0 +1,447 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, DatePicker, Input, Row, Select, Switch, TimePicker} from "antd";
import * as RoomBackend from "./backend/RoomBackend";
import {CopyOutlined, LinkOutlined} from "@ant-design/icons";
import * as ConferenceBackend from "./backend/ConferenceBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import ParticipantTable from "./ParticipantTable";
import SlotTable from "./SlotTable";
import moment from "moment";
import Video from "./Video";
import copy from "copy-to-clipboard";
const {Option} = Select;
class RoomEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
userName: props.match.params.userName,
roomName: props.match.params.roomName,
room: null,
conferences: [],
};
}
UNSAFE_componentWillMount() {
this.getRoom();
this.getGlobalConferences();
}
getGlobalConferences() {
ConferenceBackend.getGlobalConferences()
.then(res => {
this.setState({
conferences: res,
});
});
}
getRoom() {
RoomBackend.getRoom(this.state.userName, this.state.roomName)
.then((room) => {
this.setState({
room: room,
});
});
}
parseRoomField(key, value) {
if (["videoWidth", "videoHeight"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateRoomField(key, value) {
value = this.parseRoomField(key, value);
const room = this.state.room;
room[key] = value;
this.setState({
room: room,
});
}
renderRoom() {
return (
<Card size="small" title={
<div>
{i18next.t("room:Edit Room")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={this.submitRoomEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("submission:Conference")}:
</Col>
<Col span={2} >
<Select virtual={false} style={{width: "100%"}} value={this.state.room.conference} onChange={(value => {this.updateRoomField("conference", value);})}>
{
this.state.conferences.map((conference, index) => <Option key={index} value={conference.name}>{conference.name}</Option>)
}
</Select>
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Name")}:
</Col>
<Col span={2} >
<Input value={this.state.room.name} onChange={e => {
this.updateRoomField("name", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Display name")}:
</Col>
<Col span={3} >
<Input value={this.state.room.displayName} onChange={e => {
this.updateRoomField("displayName", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Speaker")}:
</Col>
<Col span={6} >
<Input value={this.state.room.speaker} onChange={e => {
this.updateRoomField("speaker", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Date")}:
</Col>
<Col span={3} >
<DatePicker defaultValue={moment(this.state.room.date, "YYYY-MM-DD")} onChange={(time, timeString) => {
this.updateRoomField("date", timeString);
}} />
</Col>
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Start time")}:
</Col>
<Col span={3} >
<TimePicker value={moment(this.state.room.startTime, "HH:mm")} format={"HH:mm"} onChange={(time, timeString) => {
this.updateRoomField("startTime", timeString);
}} />
</Col>
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:End time")}:
</Col>
<Col span={4} >
<TimePicker value={moment(this.state.room.endTime, "HH:mm")} format={"HH:mm"} onChange={(time, timeString) => {
this.updateRoomField("endTime", timeString);
}} />
</Col>
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Location")}:
</Col>
<Col span={6} >
<Input value={this.state.room.location} onChange={e => {
this.updateRoomField("location", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:SDK key")}:
</Col>
<Col span={7} >
<Input value={this.state.room.sdkKey} onChange={e => {
this.updateRoomField("sdkKey", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Signature")}:
</Col>
<Col span={12} >
<Input value={this.state.room.signature} onChange={e => {
this.updateRoomField("signature", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Meeting No.")}:
</Col>
<Col span={2} >
<Input value={this.state.room.meetingNumber} onChange={e => {
this.updateRoomField("meetingNumber", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Passcode")}:
</Col>
<Col span={2} >
<Input value={this.state.room.passcode} onChange={e => {
this.updateRoomField("passcode", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Invite link")}:
</Col>
<Col span={12} >
<Input prefix={<LinkOutlined />} value={this.state.room.inviteLink} onChange={e => {
this.updateRoomField("inviteLink", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Start URL")}:
</Col>
<Col span={7} >
<Input prefix={<LinkOutlined />} value={this.state.room.startUrl} onChange={e => {
this.updateRoomField("startUrl", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Image URL")}:
</Col>
<Col span={12} >
<Input prefix={<LinkOutlined />} value={this.state.room.imageUrl} onChange={e => {
this.updateRoomField("imageUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Ingest domain")}:
</Col>
<Col span={3} >
<Input prefix={<LinkOutlined />} value={this.state.room.ingestDomain} onChange={e => {
this.updateRoomField("ingestDomain", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Ingest auth key")}:
</Col>
<Col span={6} >
<Input value={this.state.room.ingestAuthKey} onChange={e => {
this.updateRoomField("ingestAuthKey", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Video width")}:
</Col>
<Col span={2} >
<Input value={this.state.room.videoWidth} onChange={e => {
this.updateRoomField("videoWidth", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Video height")}:
</Col>
<Col span={2} >
<Input value={this.state.room.videoHeight} onChange={e => {
this.updateRoomField("videoHeight", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Ingest URL")}:
</Col>
<Col span={22} >
<Input.Group compact>
<Input style={{width: "900px"}} prefix={<LinkOutlined />} disabled={true} value={Setting.getIngestUrl(this.state.room)} onChange={e => {}} />
<Button icon={<CopyOutlined />} onClick={() => {
const ingestUrl = Setting.getIngestUrl(this.state.room);
copy(ingestUrl);
Setting.showMessage("success", `Ingest URL copied to clipboard successfully: ${ingestUrl}`);
}} />
</Input.Group>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Streaming domain")}:
</Col>
<Col span={3} >
<Input prefix={<LinkOutlined />} value={this.state.room.streamingDomain} onChange={e => {
this.updateRoomField("streamingDomain", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Streaming auth key")}:
</Col>
<Col span={6} >
<Input value={this.state.room.streamingAuthKey} onChange={e => {
this.updateRoomField("streamingAuthKey", e.target.value);
}} />
</Col>
<Col span={1} />
<Col style={{marginTop: "5px"}} span={3}>
{i18next.t("room:Mobile streaming auth key")}:
</Col>
<Col span={6} >
<Input value={this.state.room.mobileStreamingAuthKey} onChange={e => {
this.updateRoomField("mobileStreamingAuthKey", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Streaming URL")}:
</Col>
<Col span={22} >
<Input.Group compact>
<Input style={{width: "900px"}} prefix={<LinkOutlined />} disabled={true} value={Setting.getStreamingUrl(this.state.room)} onChange={e => {}} />
<Button icon={<CopyOutlined />} onClick={() => {
const streamingUrl = Setting.getStreamingUrl(this.state.room);
copy(streamingUrl);
Setting.showMessage("success", `Streaming URL copied to clipboard successfully: ${streamingUrl}`);
}} />
</Input.Group>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Video URL")}:
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.room.videoUrl} onChange={e => {
this.updateRoomField("videoUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("room:Participants")}:
</Col>
<Col span={22} >
<ParticipantTable
title={i18next.t("room:Participants")}
table={this.state.room.participants}
room={this.state.room}
onUpdateTable={(value) => {this.updateRoomField("participants", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("room:Slots")}:
</Col>
<Col span={22} >
<SlotTable
title={i18next.t("room:Slots")}
table={this.state.room.slots}
onUpdateTable={(value) => {this.updateRoomField("slots", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Status")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.room.status} onChange={(value => {this.updateRoomField("status", value);})}>
{
[
{id: "Started", name: i18next.t("room:Started")},
{id: "Ended", name: i18next.t("room:Ended")},
{id: "Hidden", name: i18next.t("room:Hidden")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("room:Is public")}:
</Col>
<Col span={1} >
<Switch checked={this.state.room.isPublic} onChange={checked => {
this.updateRoomField("isPublic", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
{
!this.state.room.isLive ? null : (
<Video room={this.state.room} />
)
}
</Col>
</Row>
</Card>
);
}
submitRoomEdit() {
const room = Setting.deepCopy(this.state.room);
RoomBackend.updateRoom(this.state.room.owner, this.state.roomName, room)
.then((res) => {
if (res) {
Setting.showMessage("success", "Successfully saved");
this.setState({
roomName: this.state.room.name,
});
this.props.history.push(`/rooms/${this.state.room.owner}/${this.state.room.name}`);
} else {
Setting.showMessage("error", "failed to save: server side failure");
this.updateRoomField("name", this.state.roomName);
}
})
.catch(error => {
Setting.showMessage("error", `failed to save: ${error}`);
});
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.state.room !== null ? this.renderRoom() : null
}
</Col>
<Col span={1}>
</Col>
</Row>
<Row style={{margin: 10}}>
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large" onClick={this.submitRoomEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>
);
}
}
export default RoomEditPage;

497
web/src/RoomListPage.js Normal file
View File

@ -0,0 +1,497 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Card, Col, Modal, Popconfirm, Row, Spin, Switch, Table, Tooltip} from "antd";
import {CloseCircleTwoTone, VideoCameraOutlined} from "@ant-design/icons";
import moment from "moment";
import * as Setting from "./Setting";
import * as Conf from "./Conf";
import * as RoomBackend from "./backend/RoomBackend";
import i18next from "i18next";
import QrCode from "./QrCode";
import RoomCard from "./RoomCard";
import * as PaymentBackend from "./backend/PaymentBackend";
class RoomListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
rooms: null,
payments: null,
isRoomCalendar: !Setting.isAdminUser(this.props.account) ? true : Setting.getIsRoomCalendar(),
};
}
UNSAFE_componentWillMount() {
this.getGlobalRooms();
if (this.props.account !== undefined && this.props.account !== null) {
this.getPayments();
}
}
getGlobalRooms() {
RoomBackend.getGlobalRooms(this.props.isPublic)
.then((res) => {
this.setState({
rooms: res,
});
});
}
getPayments() {
PaymentBackend.getPayments(this.props.account.name)
.then((payments) => {
this.setState({
payments: payments.filter(payment => (payment.state === "Paid" && !payment.message.includes("Refund"))),
});
});
}
newRoom() {
return {
owner: this.props.account.name,
name: `room_${this.state.rooms.length}`,
createdTime: moment().format(),
displayName: `New Room - ${this.state.rooms.length}`,
conference: Conf.DefaultConferenceName,
speaker: "Alice",
Date: "2022-03-21",
startTime: "09:30",
endTime: "11:30",
location: "City Town",
imageUrl: "https://cdn.casbin.com/casdoor/resource/built-in/admin/picture.jpg",
meetingNumber: "123456789",
passcode: "123456",
inviteLink: "https://zoom.us/j/123456789?pwd=123456",
participants: [],
slots: [],
status: "Ended",
isPublic: false,
ingestDomain: "",
ingestAuthKey: "",
streamingDomain: "",
streamingAuthKey: "",
videoWidth: 1280,
videoHeight: 720,
isLive: false,
liveUserCount: 0,
viewerCount: 0,
};
}
addRoom() {
const newRoom = this.newRoom();
RoomBackend.addRoom(newRoom)
.then((res) => {
Setting.showMessage("success", "Room added successfully");
this.setState({
rooms: Setting.prependRow(this.state.rooms, newRoom),
});
}
)
.catch(error => {
Setting.showMessage("error", `Room failed to add: ${error}`);
});
}
deleteRoom(i) {
RoomBackend.deleteRoom(this.state.rooms[i])
.then((res) => {
Setting.showMessage("success", "Room deleted successfully");
this.setState({
rooms: Setting.deleteRow(this.state.rooms, i),
});
}
)
.catch(error => {
Setting.showMessage("error", `Room failed to delete: ${error}`);
});
}
registerRoom(i) {
const room = this.state.rooms[i];
RoomBackend.registerRoom("admin", room.name)
.then((room) => {
this.getGlobalRooms();
});
}
renderTable(rooms) {
const columns = [
// {
// title: i18next.t("general:Owner"),
// dataIndex: 'owner',
// key: 'owner',
// width: '70px',
// sorter: (a, b) => a.owner.localeCompare(b.owner),
// render: (text, record, index) => {
// return (
// <a target="_blank" rel="noreferrer" href={Setting.getUserProfileUrl(text)}>
// {text}
// </a>
// )
// }
// },
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "90px",
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record, index) => {
return (
<Link to={`/rooms/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "120px",
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
},
// {
// title: i18next.t("submission:Conference"),
// dataIndex: 'conference',
// key: 'conference',
// width: '110px',
// sorter: (a, b) => a.conference.localeCompare(b.conference),
// render: (text, record, index) => {
// if (Setting.isAdminUser(this.props.account)) {
// return (
// <Link to={`/conferences/${text}`}>
// {text}
// </Link>
// )
// } else {
// return text;
// }
// }
// },
// {
// title: i18next.t("general:Created time"),
// dataIndex: 'createdTime',
// key: 'createdTime',
// width: '110px',
// sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
// render: (text, record, index) => {
// return Setting.getFormattedDate(text);
// }
// },
{
title: i18next.t("room:Speaker"),
dataIndex: "speaker",
key: "speaker",
width: "250px",
sorter: (a, b) => a.speaker.localeCompare(b.speaker),
},
{
title: i18next.t("room:Date"),
dataIndex: "date",
key: "date",
width: "80px",
sorter: (a, b) => a.date.localeCompare(b.date),
},
{
title: i18next.t("room:Time"),
dataIndex: "time",
key: "time",
width: "90px",
sorter: (a, b) => a.time.localeCompare(b.time),
render: (text, record, index) => {
return `${record.startTime} - ${record.endTime}`;
},
},
{
title: i18next.t("room:Location"),
dataIndex: "location",
key: "location",
width: "120px",
sorter: (a, b) => a.location.localeCompare(b.location),
},
{
title: i18next.t("room:Meeting No."),
dataIndex: "meetingNumber",
key: "meetingNumber",
width: "120px",
sorter: (a, b) => a.meetingNumber.localeCompare(b.meetingNumber),
},
{
title: i18next.t("room:Passcode"),
dataIndex: "passcode",
key: "passcode",
width: "80px",
sorter: (a, b) => a.passcode.localeCompare(b.passcode),
},
{
title: i18next.t("general:Status"),
dataIndex: "status",
key: "status",
width: "90px",
sorter: (a, b) => a.status.localeCompare(b.status),
render: (text, record, index) => {
if (text === "Started") {
return i18next.t("room:Started");
} else if (text === "Ended") {
return i18next.t("room:Ended");
} else if (text === "Hidden") {
return i18next.t("room:Hidden");
} else {
return text;
}
},
},
{
title: i18next.t("room:Is public"),
dataIndex: "isPublic",
key: "isPublic",
width: "90px",
sorter: (a, b) => a.isPublic - b.isPublic,
render: (text, record, index) => {
return (
<Switch checked={text} disabled={true} />
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "270px",
render: (text, room, index) => {
const startUrl = room.startUrl;
if (Setting.isAdminUser(this.props.account)) {
return (
<div>
<a target="_blank" rel="noreferrer" href={startUrl}>
<Button disabled={startUrl === ""} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} danger>
{
room.status === "Started" ? i18next.t("room:Join In") :
i18next.t("room:Start Meeting")
}
</Button>
</a>
{
(startUrl === "") ? (
<Button disabled={startUrl === ""} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} danger>{i18next.t("room:Scan QR Code")}</Button>
) : (
<Tooltip placement="topLeft" color={"white"} overlayStyle={{maxWidth: "1000px"}} title={<QrCode url={startUrl} />}>
<Button disabled={startUrl === ""} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} danger>{i18next.t("room:Scan QR Code")}</Button>
</Tooltip>
)
}
<Button disabled={!room.isLive} icon={<VideoCameraOutlined />} style={{marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>
{i18next.t("room:Watch Live")}
{Setting.getRoomLiveUserCount(room)}
</Button>
{/* <Button disabled={room.isLive || room.videoUrl === ""} icon={<PlayCircleOutlined />} style={{marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}/view`)}>{i18next.t("room:Watch Playback")}</Button>*/}
<Button style={{marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${room.owner}/${room.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete room: ${room.name} ?`}
onConfirm={() => this.deleteRoom(index)}
okText="OK"
cancelText="Cancel"
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
} else {
return null;
}
},
},
];
return (
<div>
<Table columns={columns} dataSource={rooms} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{
this.props.isPublic ? i18next.t("general:Public Rooms") :
i18next.t("general:Rooms")
}&nbsp;&nbsp;&nbsp;&nbsp;
{
!Setting.isAdminUser(this.props.account) ? null : (
<React.Fragment>
<Button type="primary" size="small" onClick={this.addRoom.bind(this)}>{i18next.t("general:Add")}</Button>
&nbsp;&nbsp;&nbsp;&nbsp;
{
this.renderCalendarModeSwitch()
}
</React.Fragment>
)
}
</div>
)}
loading={rooms === null}
/>
</div>
);
}
renderCalendarModeSwitch() {
return (
<React.Fragment>
{i18next.t("room:Calendar mode")}:
&nbsp;
<Switch checked={this.state.isRoomCalendar} onChange={(checked, e) => {
this.setState({
isRoomCalendar: checked,
});
Setting.setIsRoomCalendar(checked);
}} />
</React.Fragment>
);
}
renderCard(index, room, isSingle) {
const time = (room.startTime === "") ? "" : `${room.startTime} - ${room.endTime}, ${room.location}`;
return (
<RoomCard logo={room.imageUrl} link={room.startUrl} title={room.displayName} desc={room.speaker} time={time} isSingle={isSingle} key={room.name} index={index} room={room} account={this.props.account} payments={this.state.payments} onRegisterRoom={(i) => {this.registerRoom(i);}} />
);
}
renderCards(rooms) {
if (rooms === null) {
return (
<Spin spinning={true} size="large" tip={i18next.t("general:Loading...")} style={{paddingTop: "10%"}} />
);
}
const isSingle = rooms.length === 1;
if (Setting.isMobile()) {
return (
<Card bodyStyle={{padding: 0}}>
{
rooms.map((room, i) => {
return this.renderCard(i, room, isSingle);
})
}
</Card>
);
} else {
return (
<div style={{marginRight: "15px", marginLeft: "15px"}}>
<Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}>
{
rooms.map((room, i) => {
return this.renderCard(i, room, isSingle);
})
}
</Row>
</div>
);
}
}
renderCalendar(rooms) {
return (
<React.Fragment>
{
!Setting.isAdminUser(this.props.account) ? null : (
<React.Fragment>
{
this.renderCalendarModeSwitch()
}
<br />
</React.Fragment>
)
}
<Row style={{width: "100%"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
{
this.renderCards(rooms)
}
</Col>
</Row>
</React.Fragment>
);
}
renderPaymentModal() {
if (!Conf.isPaymentRequired) {
return null;
}
if (this.state.payments === null) {
return null;
}
if (this.props.isPublic) {
return null;
}
if (Setting.isMeetingUser(this.props.account, this.state.payments)) {
return null;
}
const handleOk = () => {
this.props.history.push("/payments");
};
return (
<Modal
title={
<div>
<CloseCircleTwoTone twoToneColor="rgb(255,77,79)" />
{" " + i18next.t("room:You need to pay first to enter meeting rooms")}
</div>
}
visible={true}
cancelButtonProps={{
style: {
display: "none",
},
}}
onOk={handleOk}
onCancel={() => {}}
okText={i18next.t("room:Go to Pay")}
closable={false}
>
<div>
{i18next.t("room:In the 'Payments' page, please select the 'Online Participation Rate` tier (2nd row) to be able to access the online meeting rooms.")}
</div>
</Modal>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={24}>
{
this.state.isRoomCalendar ? this.renderCalendar(this.state.rooms) : this.renderTable(this.state.rooms)
}
</Col>
{
this.renderPaymentModal()
}
</Row>
</div>
);
}
}
export default RoomListPage;

193
web/src/RoomPage.js Normal file
View File

@ -0,0 +1,193 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Row} from "antd";
import * as RoomBackend from "./backend/RoomBackend";
import * as Setting from "./Setting";
import Video from "./Video";
import i18next from "i18next";
import * as Conf from "./Conf";
class RoomPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
userName: props.match?.params.userName,
roomName: props.match?.params.roomName,
slotName: props.match?.params.slotName,
room: null,
};
}
UNSAFE_componentWillMount() {
this.getRoom();
if (this.state.slotName === undefined) {
this.incrementRoomViewer();
}
}
getRoom() {
RoomBackend.getRoom(this.state.userName, this.state.roomName)
.then((room) => {
this.setState({
room: room,
});
});
}
incrementRoomViewer() {
RoomBackend.incrementRoomViewer(this.state.userName, this.state.roomName)
.then((res) => {
if (!res) {
Setting.showMessage("error", "Room failed to increment viewer");
}
});
}
getPropsOrStateRoom() {
return this.props.room !== undefined ? this.props.room : this.state.room;
}
getSlot(room) {
if (this.state.slotName === undefined) {
return null;
} else {
return room.slots[this.state.slotName];
}
}
getRoomTitle(room) {
let type;
if (room.isLive) {
type = i18next.t("room:Live");
const viewers = i18next.t("room:viewers");
return `${Setting.getLanguageText(room.displayName)} (${type}, ${viewers}: ${room.viewerCount})`;
} else if (room.videoUrl !== "") {
type = i18next.t("room:Playback");
} else if (this.state.slotName !== undefined) {
const slot = this.getSlot(room);
if (slot !== null && slot.videoUrl !== "") {
type = i18next.t("room:Playback");
} else {
type = i18next.t("room:No Content");
}
return `${slot.title} (${type})`;
} else {
type = i18next.t("room:No Content");
}
return `${Setting.getLanguageText(room.displayName)} (${type})`;
}
getRoomSubtitle(room) {
if (this.state.slotName !== undefined) {
const slot = this.getSlot(room);
return (
<React.Fragment>
<div>
{slot.speaker}
</div>
<div>
{slot.date}&nbsp;&nbsp;&nbsp;&nbsp;{`${slot.startTime}-${slot.endTime}`}
</div>
</React.Fragment>
);
} else {
return null;
}
}
renderVideo(room) {
return (
<div style={{marginTop: "10px", textAlign: "center"}}>
<div style={{fontSize: 30, marginBottom: "20px"}}>
{
this.getRoomTitle(room)
}
</div>
<div style={{fontSize: 18, marginBottom: "20px"}}>
{
this.getRoomSubtitle(room)
}
</div>
<Video room={room} slot={this.getSlot(room)} />
</div>
);
}
renderComments() {
if (this.state.room === null) {
return null;
}
const nodeId = `comments-${Conf.DefaultConferenceName}`;
let title = encodeURIComponent(`Comments - ${this.state.room.displayName}`);
if (this.state.slotName !== undefined) {
title += ` - ${this.state.slotName}`;
}
const author = (this.props.account === null) ? "admin" : this.props.account.name;
const urlPath = encodeURIComponent(`|comment|${this.state.room.displayName}`);
let accessToken;
if (this.props.account === null) {
// Confita is signed out, also sign out Casnode.
accessToken = "signout";
} else {
accessToken = this.props.account.accessToken;
}
const width = !Setting.isMobile() ? `${this.state.room.videoWidth}px` : "100%";
return (
<iframe
key={title}
title={title}
style={{border: "1px solid rgb(232,232,232)", width: width, height: "100vh"}}
src={`${Conf.CasnodeEndpoint}/embedded-replies?nodeId=${nodeId}&title=${title}&author=${author}&urlPath=${urlPath}&accessToken=${accessToken}`}
/>
);
}
render() {
const room = this.getPropsOrStateRoom();
if (room === null) {
return null;
}
return (
<div>
<Row style={{width: "100%"}}>
<Col span={!Setting.isMobile() ? 1 : 0}>
</Col>
<Col span={!Setting.isMobile() ? 22 : 24}>
{
this.renderVideo(room)
}
<div style={{marginTop: "20px", textAlign: "center"}}>
{
this.renderComments()
}
</div>
</Col>
<Col span={!Setting.isMobile() ? 1 : 0}>
</Col>
</Row>
</div>
);
}
}
export default RoomPage;

View File

@ -0,0 +1,45 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import * as Setting from "./Setting";
import {Dropdown, Menu} from "antd";
import {createFromIconfontCN} from "@ant-design/icons";
import "./App.less";
const IconFont = createFromIconfontCN({
scriptUrl: "//at.alicdn.com/t/font_2680620_ffij16fkwdg.js",
});
const LanguageItems = [
{lang: "en", label: "English", icon: "icon-en"},
{lang: "zh", label: "中文", icon: "icon-zh"},
];
class SelectLanguageBox extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
render() {
return <Dropdown overlay={<Menu>{LanguageItems.map(({lang, label, icon}) => <Menu.Item key={lang} onClick={() => Setting.changeLanguage(lang)}><IconFont type={icon} />{label}</Menu.Item>)}</Menu>}>
<div className="language-box"></div>
</Dropdown>;
}
}
export default SelectLanguageBox;

484
web/src/Setting.js Normal file
View File

@ -0,0 +1,484 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Alert, Tag, message} from "antd";
import {EyeOutlined} from "@ant-design/icons";
import {isMobile as isMobileDevice} from "react-device-detect";
import i18next from "i18next";
import Sdk from "casdoor-js-sdk";
import XLSX from "xlsx";
export let ServerUrl = "";
export let CasdoorSdk;
export function initServerUrl() {
const hostname = window.location.hostname;
if (hostname === "localhost") {
ServerUrl = `http://${hostname}:12000`;
}
}
export function initCasdoorSdk(config) {
CasdoorSdk = new Sdk(config);
}
function getUrlWithLanguage(url) {
if (url.includes("?")) {
return `${url}&language=${getLanguage()}`;
} else {
return `${url}?language=${getLanguage()}`;
}
}
export function getSignupUrl() {
return getUrlWithLanguage(CasdoorSdk.getSignupUrl());
}
export function getSigninUrl() {
return getUrlWithLanguage(CasdoorSdk.getSigninUrl());
}
export function getUserProfileUrl(userName, account) {
return getUrlWithLanguage(CasdoorSdk.getUserProfileUrl(userName, account));
}
export function getMyProfileUrl(account) {
return getUrlWithLanguage(CasdoorSdk.getMyProfileUrl(account));
}
export function getProductBuyUrl(account, productName) {
return getMyProfileUrl(account).replace("/account", `/products/${productName}/buy`);
}
export function getPaymentUrl(account, payment) {
return getMyProfileUrl(account).replace("/account", `/payments/${payment.name}/result`);
}
export function getPaymentInvoiceUrl(account, payment) {
return getMyProfileUrl(account).replace("/account", `/payments/${payment.name}`);
}
export function signin() {
return CasdoorSdk.signin(ServerUrl);
}
export function parseJson(s) {
if (s === "") {
return null;
} else {
return JSON.parse(s);
}
}
export function myParseInt(i) {
const res = parseInt(i);
return isNaN(res) ? 0 : res;
}
export function openLink(link) {
// this.props.history.push(link);
const w = window.open("about:blank");
w.location.href = link;
}
export function openLinkSafe(link) {
// Javascript window.open issue in safari
// https://stackoverflow.com/questions/45569893/javascript-window-open-issue-in-safari
const a = document.createElement("a");
a.href = link;
a.setAttribute("target", "_blank");
a.click();
}
export function goToLink(link) {
window.location.href = link;
}
export function goToLinkSoft(ths, link) {
ths.props.history.push(link);
}
export function goToContact(ths) {
goToLinkSoft(ths, "/contact");
}
export function showMessage(type, text) {
if (type === "") {
return;
} else if (type === "success") {
message.success(text);
} else if (type === "error") {
message.error(text);
}
}
export function isAdminUser(account) {
return account?.isAdmin;
}
export function isEditorUser(account) {
return account?.tag === "Editor";
}
export function isCommitteeUser(account) {
return account?.tag === "Committee";
}
export function isBranchUser(account) {
return account?.tag === "Branch";
}
export function isPayedUser(account, payments) {
if (payments === null) {
return false;
}
return payments.filter(payment => payment.productName.includes("_standard_") || payment.productName.includes("_online_") || payment.productName.includes("_early_")).length > 0;
}
export function isMeetingUser(account, payments) {
return isAdminUser(account) || isEditorUser(account) || isCommitteeUser(account) || isBranchUser(account) || isPayedUser(account, payments);
}
export function deepCopy(obj) {
return Object.assign({}, obj);
}
export function insertRow(array, row, i) {
return [...array.slice(0, i), row, ...array.slice(i)];
}
export function addRow(array, row) {
return [...array, row];
}
export function prependRow(array, row) {
return [row, ...array];
}
export function deleteRow(array, i) {
// return array = array.slice(0, i).concat(array.slice(i + 1));
return [...array.slice(0, i), ...array.slice(i + 1)];
}
export function swapRow(array, i, j) {
return [...array.slice(0, i), array[j], ...array.slice(i + 1, j), array[i], ...array.slice(j + 1)];
}
export function trim(str, ch) {
if (str === undefined) {
return undefined;
}
let start = 0;
let end = str.length;
while (start < end && str[start] === ch) {++start;}
while (end > start && str[end - 1] === ch) {--end;}
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
export function isMobile() {
// return getIsMobileView();
return isMobileDevice;
}
export function getFormattedDate(date) {
if (date === undefined || date === null) {
return null;
}
date = date.replace("T", " ");
date = date.replace("+08:00", " ");
return date;
}
export function getFormattedDateShort(date) {
return date.slice(0, 10);
}
export function getShortName(s) {
return s.split("/").slice(-1)[0];
}
export function getShortText(s, maxLength = 35) {
if (s === undefined || s === null) {
return s;
}
if (s.length > maxLength) {
return `${s.slice(0, maxLength)}...`;
} else {
return s;
}
}
function getRandomInt(s) {
let hash = 0;
if (s.length !== 0) {
for (let i = 0; i < s.length; i++) {
const char = s.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
}
return hash;
}
export function getAvatarColor(s) {
const colorList = ["#f56a00", "#7265e6", "#ffbf00", "#00a2ae"];
let random = getRandomInt(s);
if (random < 0) {
random = -random;
}
return colorList[random % 4];
}
export function getLanguageText(text) {
if (!text.includes("|")) {
return text;
}
let res;
const tokens = text.split("|");
if (getLanguage() !== "zh") {
res = trim(tokens[0], "");
} else {
res = trim(tokens[1], "");
}
return res;
}
export function getLanguage() {
return i18next.language;
}
export function onLanguageChange(callBack) {
if (typeof callBack !== "function") {
return null;
}
i18next.on("languageChanged", callBack);
return () => i18next.off("languageChanged", callBack);
}
export function setLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);
i18next.changeLanguage(language);
}
export function changeLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);
i18next.changeLanguage(language);
// window.location.reload(true);
}
export function changeMomentLanguage(lng) {
return;
// if (lng === "zh") {
// moment.locale("zh", {
// relativeTime: {
// future: "%s内",
// past: "%s前",
// s: "几秒",
// ss: "%d秒",
// m: "1分钟",
// mm: "%d分钟",
// h: "1小时",
// hh: "%d小时",
// d: "1天",
// dd: "%d天",
// M: "1个月",
// MM: "%d个月",
// y: "1年",
// yy: "%d年",
// },
// });
// }
}
export function getIsRoomCalendar() {
const res = localStorage.getItem("isRoomCalendar");
if (res === null) {
return false;
}
return res === "true";
}
export function setIsRoomCalendar(isRoomCalendar) {
localStorage.setItem("isRoomCalendar", isRoomCalendar);
}
export function getFilenameFromUrl(url) {
if (url === undefined || url === null) {
return url;
}
const filename = url.substring(url.lastIndexOf("/") + 1);
return filename;
}
function getCurrencySymbol(product) {
if (product?.currency === "USD") {
return "$";
} else if (product?.currency === "CNY") {
return "¥";
} else {
return "(Unknown currency)";
}
}
export function getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("payment:USD");
} else if (product?.currency === "CNY") {
return i18next.t("payment:CNY");
} else {
return "(Unknown currency)";
}
}
export function getPrice(product) {
return `${getCurrencySymbol(product)}${product?.price} (${getCurrencyText(product)})`;
}
export function getState(payment) {
if (payment?.state === "Paid") {
return i18next.t("payment:Paid");
} else if (payment?.state === "Created") {
return i18next.t("payment:Created");
} else {
return "(Unknown state)";
}
}
export function getAlert(type, text) {
return (
<Alert
message={text}
type={type}
showIcon
/>
);
}
export function getTag(text, color = "processing") {
return (
<Tag color={color}>
{text}
</Tag>
);
}
function getTagColor(s) {
return "default";
}
export function getTags(tags) {
const res = [];
if (!tags) {return res;}
tags.forEach((tag, i) => {
res.push(
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
);
});
return res;
}
function s2ab(s) {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i !== s.length; i++) {
view[i] = s.charCodeAt(i) & 0xFF;
}
return buf;
}
export function sheet2blob(sheet, sheetName) {
const workbook = {
SheetNames: [sheetName],
Sheets: {},
};
workbook.Sheets[sheetName] = sheet;
return workbook2blob(workbook);
}
export function workbook2blob(workbook) {
const wopts = {
bookType: "xlsx",
bookSST: false,
type: "binary",
};
const wbout = XLSX.write(workbook, wopts);
return new Blob([s2ab(wbout)], {type: "application/octet-stream"});
}
export function getIngestUrl(room) {
return `rtmp://${room.ingestDomain}/${room.conference}/${room.name}?auth_key=${room.ingestAuthKey}`;
}
export function getStreamingUrl(room) {
return `https://${room.streamingDomain}/${room.conference}/${room.name}.flv?auth_key=${room.streamingAuthKey}`;
}
export function getMobileStreamingUrl(room) {
return `https://${room.streamingDomain}/${room.conference}/${room.name}.m3u8?auth_key=${room.mobileStreamingAuthKey}`;
}
export function getRoomLiveUserCount(room) {
if (!room.isLive) {
return "";
}
return (
<span>
&nbsp;
(
&nbsp;
<EyeOutlined />
&nbsp;
{room.viewerCount}
&nbsp;
)
</span>
);
}
export class CodeTag {
label = "";
/**
* @type {(code:any) => boolean}
*/
filter = code => true;
}
/**
*
* @returns {Record<string, (string | CodeTag)[]>}
*/
export const getCodeTags = () => ({
general: [{label: "All notebooks", filter: () => true}, "Recently Viewed", "Beginner", "NLP", "Random Forest"],
language: ["Python", "R"],
maintained: ["Scheduled notebook"],
accelerator: ["GPU", "TPU"],
types: ["Dataset", "Competition notebook"],
outputs: ["Visualizations", "Data"],
});

28
web/src/SigninPage.js Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import * as Setting from "./Setting";
class SigninPage extends React.Component {
componentDidMount() {
window.location.replace(Setting.getSigninUrl());
}
render() {
return "";
}
}
export default SigninPage;

83
web/src/Slot.js Normal file
View File

@ -0,0 +1,83 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {withRouter} from "react-router-dom";
import {Button, Timeline} from "antd";
import {PlayCircleOutlined, SyncOutlined} from "@ant-design/icons";
import * as Setting from "./Setting";
import i18next from "i18next";
class Slot extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
getSlotColor(slot) {
if (slot.type === "Plenary") {
return "red";
} else {
return "green";
}
}
getSlotDot(slot) {
if (slot.type === "Plenary") {
return null;
} else {
return <SyncOutlined spin />;
}
}
render() {
return (
<div style={{marginTop: "30px"}}>
<Timeline>
{
this.props.slots.map((slot, i) => {
if (slot.title === "") {
return null;
}
return (
<Timeline.Item key={i} color={this.getSlotColor(slot)} dot={this.getSlotDot(slot)}>
<p>
{slot.date} &nbsp;&nbsp; {Setting.getTag(`${slot.startTime}-${slot.endTime}`)}
{
(this.props.account === undefined || this.props.account === null || slot.videoUrl === "") ? null : (
<Button icon={<PlayCircleOutlined />} size={"small"} style={{marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/rooms/${this.props.room.owner}/${this.props.room.name}/${i}/view`)}>{i18next.t("room:Playback")}</Button>
)
}
</p>
<p style={{fontWeight: "bold"}}>{slot.title}</p>
<p>{slot.speaker}</p>
{
slot.location === "" ? null : (
<p>{slot.location}</p>
)
}
</Timeline.Item>
);
})
}
</Timeline>
</div>
);
}
}
export default withRouter(Slot);

246
web/src/SlotTable.js Normal file
View File

@ -0,0 +1,246 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, DatePicker, Input, Row, Select, Table, TimePicker, Tooltip} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
import moment from "moment/moment";
const {Option} = Select;
class SlotTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
parseField(key, value) {
if ([].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateField(table, index, key, value) {
value = this.parseField(key, value);
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {type: "Oral", date: "2022-11-14", startTime: "15:00", endTime: "16:00", title: "The title", speaker: "Alice", location: "CA"};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("general:No."),
dataIndex: "no",
key: "no",
width: "60px",
render: (text, record, index) => {
return (index + 1);
},
},
{
title: i18next.t("submission:Type"),
dataIndex: "type",
key: "type",
width: "100px",
render: (text, record, index) => {
// https://support.zoom.us/hc/en-us/articles/360040324512-Roles-in-a-meeting
return (
<Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {
this.updateField(table, index, "type", value);
})}>
{
[
{id: "Plenary", name: "Plenary"},
{id: "Oral", name: "Oral"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
);
},
},
{
title: i18next.t("room:Date"),
dataIndex: "date",
key: "date",
width: "140px",
render: (text, record, index) => {
return (
<DatePicker defaultValue={moment(text, "YYYY-MM-DD")} onChange={(time, timeString) => {
this.updateField(table, index, "date", timeString);
}} />
);
},
},
{
title: i18next.t("room:Start time"),
dataIndex: "startTime",
key: "startTime",
width: "100px",
render: (text, record, index) => {
return (
<TimePicker value={moment(text, "HH:mm")} format={"HH:mm"} onChange={(time, timeString) => {
this.updateField(table, index, "startTime", timeString);
}} />
);
},
},
{
title: i18next.t("room:End time"),
dataIndex: "endTime",
key: "endTime",
width: "100px",
render: (text, record, index) => {
return (
<TimePicker value={moment(text, "HH:mm")} format={"HH:mm"} onChange={(time, timeString) => {
this.updateField(table, index, "endTime", timeString);
}} />
);
},
},
{
title: i18next.t("submission:Title"),
dataIndex: "title",
key: "title",
// width: "150px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "title", e.target.value);
}} />
);
},
},
{
title: i18next.t("room:Speaker"),
dataIndex: "speaker",
key: "speaker",
width: "300px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "speaker", e.target.value);
}} />
);
},
},
// {
// title: i18next.t("room:Location"),
// dataIndex: "location",
// key: "location",
// width: "70px",
// render: (text, record, index) => {
// return (
// <Input value={text} onChange={e => {
// this.updateField(table, index, "location", e.target.value);
// }} />
// );
// },
// },
{
title: i18next.t("room:Video URL"),
dataIndex: "videoUrl",
key: "videoUrl",
width: "200px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "videoUrl", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={"Up"}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Down"}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={"Delete"}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default SlotTable;

Some files were not shown because too many files have changed in this diff Show More