Compare commits

...

36 Commits

Author SHA1 Message Date
MRGUOKING
16f427d4d7 feat: Remove useless dependencies
Signed-off-by: MRGUOKING <420919469@qq.com>

Remove useless dependencies

Signed-off-by: MRGUOKING <420919469@qq.com>
2021-09-15 00:13:43 +08:00
MRGUOKING
6ad8f9fa0b feat: Add PayPal pay function
Signed-off-by: MRGUOKING <420919469@qq.com>

Add PayPal pay function

Signed-off-by: MRGUOKING <420919469@qq.com>
2021-09-15 00:00:56 +08:00
Yang Luo
b93a29fbc6 Improve language code. 2021-09-14 01:22:13 +08:00
Yang Luo
09f430266b Improve menu key. 2021-09-13 23:56:25 +08:00
sh1luo
52d9017611 fix: swagger bug in dev mode (#291)
Signed-off-by: sh1luo <690898835@qq.com>
2021-09-12 12:09:44 +08:00
ffyuanda
c70c62f52e docs: Update README.md for backend port caution (#293) 2021-09-08 22:02:58 +08:00
Yang Luo
355b0b35d0 Fix gitee login link. 2021-09-07 21:46:44 +08:00
Yang Luo
e4846807cd Show resource list page to users. 2021-09-06 00:49:10 +08:00
Yang Luo
f4a59de3a5 Improve resource list page. 2021-09-06 00:08:16 +08:00
Yang Luo
a1b16f88d1 Add user to Resource. 2021-09-05 23:46:56 +08:00
Yang Luo
90ec8ec787 Add GetOwnerAndNameFromIdNoCheck() to fix bug. 2021-09-05 23:46:55 +08:00
github-actions[bot]
ea8971ff29 refactor: New Crowdin translations by Github Action (#276)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-09-05 22:49:59 +08:00
Yang Luo
bd41425039 Improve format. 2021-09-05 22:09:54 +08:00
sh1luo
9d9a1da07f fix: remove routers/util (#287)
Signed-off-by: sh1luo <690898835@qq.com>
2021-09-05 22:02:32 +08:00
Yang Luo
465d25a272 Improve router base.go 2021-09-05 14:44:27 +08:00
Yang Luo
ef1195960e Improve SendSms() API. 2021-09-05 13:15:38 +08:00
Yang Luo
089f4ff480 Handle error in go-sms-sender. 2021-09-05 10:56:11 +08:00
Yang Luo
88aa444ad1 Improve SendEmail() and SendSms() APIs. 2021-09-05 10:30:51 +08:00
Yang Luo
1c5ce46bd5 Refactor GetProviderFromContext(). 2021-09-05 09:44:15 +08:00
Yang Luo
14d09cad2c Support server-side upload-resource call. 2021-09-05 01:03:29 +08:00
Yang Luo
06006c87b8 Improve filter code. 2021-09-05 00:22:08 +08:00
sh1luo
a4edf47dc4 fix: improvde code logic (#285)
Signed-off-by: sh1luo <690898835@qq.com>
2021-09-04 22:20:47 +08:00
sh1luo
e68b0198f1 fix: go proxy of dockerfile (#283)
Signed-off-by: sh1luo <690898835@qq.com>
2021-09-04 22:10:16 +08:00
Yang Luo
015961bc3c Add application to Resource. 2021-09-04 16:50:26 +08:00
Yang Luo
5d98cc6ac5 Use objectKey as resource name. 2021-09-04 15:02:11 +08:00
Yang Luo
b3eec024b8 Add getInitScore(). 2021-08-30 01:06:05 +08:00
Yang Luo
eefcfd8440 Fix address null bug. 2021-08-27 23:43:43 +08:00
Yang Luo
c6b2106c94 Add bio to user. 2021-08-25 08:07:08 +08:00
sh1luo
edf621f4d5 feat: support web-auth way for wecom (#275)
Signed-off-by: sh1luo <690898835@qq.com>
2021-08-23 23:12:53 +08:00
Yang Luo
e50c6cd4b5 Add PermanentAvatar to user. 2021-08-21 23:17:33 +08:00
Yang Luo
9c3117beb0 Rename UpdateUser functions. 2021-08-21 22:54:53 +08:00
Yang Luo
4ca307564c Add proxy pkg. 2021-08-21 22:16:25 +08:00
Yang Luo
15a6f64fdc Add 5 new user properties. 2021-08-21 10:58:34 +08:00
sh1luo
75e917a070 feat: add gitlab provider (#273)
Signed-off-by: sh1luo <690898835@qq.com>
2021-08-19 22:13:40 +08:00
github-actions[bot]
e1182bb635 refactor: New Crowdin translations by Github Action (#265)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-08-18 21:05:31 +08:00
ffyuanda
2b70698c2a docs: updated README.md for npm RAM caution (#272)
Signed-off-by: ffyuanda <46557895+ffyuanda@users.noreply.github.com>
2021-08-18 20:07:39 +08:00
77 changed files with 2023 additions and 514 deletions

View File

@@ -31,6 +31,7 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
needs: [ frontend, backend ]
steps:
- name: Checkout

View File

@@ -1,7 +1,7 @@
FROM golang:1.16 AS BACK
WORKDIR /go/src/casdoor
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server . \
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server . \
&& apt update && apt install wait-for-it && chmod +x /usr/bin/wait-for-it
FROM node:14.17.4 AS FRONT

View File

@@ -87,6 +87,7 @@ Edit `conf/app.conf`, set `runmode=dev`. Firstly build front-end files:
```bash
cd web/ && npm install && npm run start
```
*❗ A word of caution ❗: the `npm` commands above need a recommended system RAM of at least 4GB. It has a potential failure during building the files if your RAM is not sufficient.*
Then build back-end binary file, change directory to root(Relative to casdoor):
@@ -94,7 +95,8 @@ Then build back-end binary file, change directory to root(Relative to casdoor):
go run main.go
```
That's it! Try to visit http://127.0.0.1:7001/. :small_airplane:
That's it! Try to visit http://127.0.0.1:7001/. :small_airplane:
**But make sure you always request the backend port 8000 when you are using SDKs.**
##### Production Mode

View File

@@ -71,6 +71,7 @@ m = (r.subOwner == p.subOwner || p.subOwner == "*") && \
if true {
ruleText := `
p, built-in, *, *, *, *, *
p, app, *, *, *, *, *
p, *, *, POST, /api/signup, *, *
p, *, *, POST, /api/get-email-and-phone, *, *
p, *, *, POST, /api/login, *, *
@@ -84,12 +85,17 @@ p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-organizations, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-default-providers, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, POST, /api/upload-avatar, *, *
p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-human-check, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, POST, /api/paypal, *, *
p, *, *, GET, /api/success-pay, *, *
p, *, *, GET, /api/get-application-clientId, *, *
`
sa := stringadapter.NewAdapter(ruleText)

View File

@@ -7,6 +7,7 @@ driverName = mysql
dataSourceName = root:123@tcp(localhost:3306)/
dbName = casdoor
redisEndpoint =
defaultStorageProvider =
authState = "casdoor"
httpProxy = "127.0.0.1:10808"
verificationCodeTimeout = 10

View File

@@ -148,6 +148,7 @@ func (c *ApiController) Signup() {
Address: []string{},
Affiliation: form.Affiliation,
Region: form.Region,
Score: getInitScore(),
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,

View File

@@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"github.com/casbin/casdoor/object"
)
@@ -116,3 +117,10 @@ func (c *ApiController) DeleteApplication() {
c.Data["json"] = wrapActionResponse(object.DeleteApplication(&application))
c.ServeJSON()
}
func (c *ApiController) GetApplicationByClientId() {
clientId := c.Input().Get("clientId")
c.Data["json"] = object.GetApplicationByClientId(clientId)
c.ServeJSON()
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/astaxie/beego"
"github.com/casbin/casdoor/idp"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/proxy"
"github.com/casbin/casdoor/util"
)
@@ -99,9 +100,9 @@ func (c *ApiController) GetApplicationLogin() {
func setHttpClient(idProvider idp.IdProvider, providerType string) {
if providerType == "GitHub" || providerType == "Google" || providerType == "Facebook" || providerType == "LinkedIn" {
idProvider.SetHttpClient(proxyHttpClient)
idProvider.SetHttpClient(proxy.ProxyHttpClient)
} else {
idProvider.SetHttpClient(defaultHttpClient)
idProvider.SetHttpClient(proxy.DefaultHttpClient)
}
}
@@ -276,12 +277,6 @@ func (c *ApiController) Login() {
return
}
var score int
score, err = strconv.Atoi(beego.AppConfig.String("initScore"))
if err != nil {
panic(err)
}
properties := map[string]string{}
properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2)
user := &object.User{
@@ -292,8 +287,9 @@ func (c *ApiController) Login() {
Type: "normal-user",
DisplayName: userInfo.DisplayName,
Avatar: userInfo.AvatarUrl,
Address: []string{},
Email: userInfo.Email,
Score: score,
Score: getInitScore(),
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,

45
controllers/payment.go Normal file
View File

@@ -0,0 +1,45 @@
package controllers
import (
"encoding/json"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/payment"
)
func (c *ApiController) PaypalPay() {
clientId := c.Input().Get("clientId")
redirectUri := c.Input().Get("redirectUri")
var payItem object.PayItem
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payItem)
if err != nil {
panic(err)
}
msg := payment.Paypal(payItem, clientId, redirectUri)
c.Data["json"] = msg
c.ServeJSON()
}
func (c *ApiController) GetPayments() {
c.Data["json"] = object.GetPayments()
c.ServeJSON()
}
func (c *ApiController) DeletePayment() {
var payment object.Payment
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeletePayment(&payment))
c.ServeJSON()
}
func (c *ApiController) SuccessPay() {
token := c.Input().Get("paymentId")
c.Data["json"] = payment.SuccessPay(token)
c.ServeJSON()
}

View File

@@ -19,8 +19,8 @@ import (
"encoding/json"
"fmt"
"io"
"mime"
"path/filepath"
"strings"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
@@ -28,8 +28,9 @@ import (
func (c *ApiController) GetResources() {
owner := c.Input().Get("owner")
user := c.Input().Get("user")
c.Data["json"] = object.GetResources(owner)
c.Data["json"] = object.GetResources(owner, user)
c.ServeJSON()
}
@@ -64,32 +65,6 @@ func (c *ApiController) AddResource() {
c.ServeJSON()
}
func (c *ApiController) GetProviderParam() (*object.Provider, *object.User, bool) {
providerName := c.Input().Get("provider")
if providerName != "" {
provider := object.GetProvider(util.GetId(providerName))
if provider == nil {
c.ResponseError(fmt.Sprintf("The provider: %s is not found", providerName))
return nil, nil, false
}
return provider, nil, true
}
userId, ok := c.RequireSignedIn()
if !ok {
return nil, nil, false
}
user := object.GetUser(userId)
application := object.GetApplicationByUser(user)
provider := application.GetStorageProvider()
if provider == nil {
c.ResponseError(fmt.Sprintf("No storage provider is found for application: %s", application.Name))
return nil, nil, false
}
return provider, user, true
}
func (c *ApiController) DeleteResource() {
var resource object.Resource
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
@@ -97,12 +72,12 @@ func (c *ApiController) DeleteResource() {
panic(err)
}
provider, _, ok := c.GetProviderParam()
provider, _, ok := c.GetProviderFromContext("Storage")
if !ok {
return
}
err = object.DeleteFile(provider, resource.ObjectKey)
err = object.DeleteFile(provider, resource.Name)
if err != nil {
c.ResponseError(err.Error())
return
@@ -114,16 +89,18 @@ func (c *ApiController) DeleteResource() {
func (c *ApiController) UploadResource() {
owner := c.Input().Get("owner")
username := c.Input().Get("user")
application := c.Input().Get("application")
tag := c.Input().Get("tag")
parent := c.Input().Get("parent")
fullFilePath := c.Input().Get("fullFilePath")
file, header, err := c.GetFile("file")
defer file.Close()
if err != nil {
c.ResponseError(err.Error())
return
}
defer file.Close()
filename := filepath.Base(fullFilePath)
fileBuffer := bytes.NewBuffer(nil)
@@ -132,17 +109,19 @@ func (c *ApiController) UploadResource() {
return
}
provider, user, ok := c.GetProviderParam()
provider, user, ok := c.GetProviderFromContext("Storage")
if !ok {
return
}
fileType := "unknown"
contentType := header.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "image/") {
fileType = "image"
} else if strings.HasPrefix(contentType, "video/") {
fileType = "video"
fileType, _ = util.GetOwnerAndNameFromId(contentType)
if fileType != "image" && fileType != "video" {
ext := filepath.Ext(filename)
mimeType := mime.TypeByExtension(ext)
fileType, _ = util.GetOwnerAndNameFromId(mimeType)
}
fileUrl, objectKey, err := object.UploadFile(provider, fullFilePath, fileBuffer)
@@ -155,21 +134,28 @@ func (c *ApiController) UploadResource() {
fileSize := int(header.Size)
resource := &object.Resource{
Owner: owner,
Name: filename,
Name: objectKey,
CreatedTime: util.GetCurrentTime(),
User: username,
Provider: provider.Name,
Application: application,
Tag: tag,
Parent: parent,
FileName: filename,
FileType: fileType,
FileFormat: fileFormat,
FileSize: fileSize,
Url: fileUrl,
ObjectKey: objectKey,
}
object.AddOrUpdateResource(resource)
switch tag {
case "avatar":
if user == nil {
c.ResponseError("user is nil for tag: \"avatar\"")
return
}
user.Avatar = fileUrl
object.UpdateUser(user.GetId(), user)
case "termsOfUse":
@@ -179,5 +165,5 @@ func (c *ApiController) UploadResource() {
object.UpdateApplication(applicationId, app)
}
c.ResponseOk(fileUrl)
c.ResponseOk(fileUrl, objectKey)
}

View File

@@ -19,126 +19,87 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
sender "github.com/casdoor/go-sms-sender"
)
// SendEmail
// @Title SendEmail
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param body body emailForm true "Details of the email request"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param body body emailForm true "Details of the email request"
// @Success 200 {object} Response object
// @router /api/send-email [post]
func (c *ApiController) SendEmail() {
clientId := c.Input().Get("clientId")
clientSecret := c.Input().Get("clientSecret")
app := object.GetApplicationByClientIdAndSecret(clientId, clientSecret)
if app == nil {
c.ResponseError("Invalid clientId or clientSecret.")
return
}
provider := app.GetEmailProvider()
if provider == nil {
c.ResponseError("No Email provider is found")
provider, _, ok := c.GetProviderFromContext("Email")
if !ok {
return
}
var emailForm struct {
Title string `json:"title"`
Content string `json:"content"`
Receivers []string `json:"receivers"`
Sender string `json:"sender"`
Receivers []string `json:"receivers"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
if err != nil {
c.ResponseError("Request body error.")
c.ResponseError(err.Error())
return
}
if util.IsStrsEmpty(emailForm.Title, emailForm.Content, emailForm.Sender) {
c.ResponseError("Missing parameters.")
c.ResponseError(fmt.Sprintf("Empty parameters for emailForm: %v", emailForm))
return
}
var invalidEmails []string
invalidReceivers := []string{}
for _, receiver := range emailForm.Receivers {
if !util.IsEmailValid(receiver) {
invalidEmails = append(invalidEmails, receiver)
invalidReceivers = append(invalidReceivers, receiver)
}
}
if len(invalidEmails) != 0 {
c.ResponseError("Invalid Email addresses", invalidEmails)
if len(invalidReceivers) != 0 {
c.ResponseError(fmt.Sprintf("Invalid Email receivers: %s", invalidReceivers))
return
}
ok := 0
for _, receiver := range emailForm.Receivers {
if msg := object.SendEmail(
provider,
emailForm.Title,
emailForm.Content,
receiver,
emailForm.Sender); len(msg) == 0 {
ok++
err = object.SendEmail(provider, emailForm.Title, emailForm.Content, receiver, emailForm.Sender)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.Data["json"] = Response{Status: "ok", Data: ok}
c.ServeJSON()
c.ResponseOk()
}
// SendSms
// @Title SendSms
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param body body smsForm true "Details of the sms request"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param body body smsForm true "Details of the sms request"
// @Success 200 {object} Response object
// @router /api/send-sms [post]
func (c *ApiController) SendSms() {
clientId := c.Input().Get("clientId")
clientSecret := c.Input().Get("clientSecret")
app := object.GetApplicationByClientIdAndSecret(clientId, clientSecret)
if app == nil {
c.ResponseError("Invalid clientId or clientSecret.")
return
}
provider := app.GetSmsProvider()
if provider == nil {
c.ResponseError("No SMS provider is found")
return
}
client := sender.NewSmsClient(
provider.Type,
provider.ClientId,
provider.ClientSecret,
provider.SignName,
provider.RegionId,
provider.TemplateCode,
provider.AppId,
)
if client == nil {
c.ResponseError("Invalid provider info.")
provider, _, ok := c.GetProviderFromContext("SMS")
if !ok {
return
}
var smsForm struct {
Receivers []string `json:"receivers"`
Parameters map[string]string `json:"parameters"`
Content string `json:"content"`
Receivers []string `json:"receivers"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
if err != nil {
c.ResponseError("Request body error.")
c.ResponseError(err.Error())
return
}
@@ -150,11 +111,15 @@ func (c *ApiController) SendSms() {
}
if len(invalidReceivers) != 0 {
c.ResponseError("Invalid phone numbers", invalidReceivers)
c.ResponseError(fmt.Sprintf("Invalid phone receivers: %s", invalidReceivers))
return
}
client.SendMessage(smsForm.Parameters, smsForm.Receivers...)
c.Data["json"] = Response{Status: "ok"}
c.ServeJSON()
err = object.SendSms(provider, smsForm.Content, smsForm.Receivers...)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk()
}

View File

@@ -15,45 +15,14 @@
package controllers
import (
"net/http"
"fmt"
"strconv"
"github.com/astaxie/beego"
"golang.org/x/net/proxy"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
var defaultHttpClient *http.Client
var proxyHttpClient *http.Client
func InitHttpClient() {
// not use proxy
defaultHttpClient = http.DefaultClient
// use proxy
httpProxy := beego.AppConfig.String("httpProxy")
if httpProxy == "" {
proxyHttpClient = &http.Client{}
return
}
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", httpProxy, nil, proxy.Direct)
if err != nil {
panic(err)
}
tr := &http.Transport{Dial: dialer.Dial}
proxyHttpClient = &http.Client{
Transport: tr,
}
//resp, err2 := proxyHttpClient.Get("https://google.com")
//if err2 != nil {
// panic(err2)
//}
//defer resp.Body.Close()
//println("Response status: %s", resp.Status)
}
// ResponseOk ...
func (c *ApiController) ResponseOk(data ...interface{}) {
resp := Response{Status: "ok"}
@@ -91,3 +60,43 @@ func (c *ApiController) RequireSignedIn() (string, bool) {
}
return userId, true
}
func getInitScore() int {
score, err := strconv.Atoi(beego.AppConfig.String("initScore"))
if err != nil {
panic(err)
}
return score
}
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, *object.User, bool) {
providerName := c.Input().Get("provider")
if providerName != "" {
provider := object.GetProvider(util.GetId(providerName))
if provider == nil {
c.ResponseError(fmt.Sprintf("The provider: %s is not found", providerName))
return nil, nil, false
}
return provider, nil, true
}
userId, ok := c.RequireSignedIn()
if !ok {
return nil, nil, false
}
application, user := object.GetApplicationByUserId(userId)
if application == nil {
c.ResponseError(fmt.Sprintf("No application is found for userId: \"%s\"", userId))
return nil, nil, false
}
provider := application.GetProviderByCategory(category)
if provider == nil {
c.ResponseError(fmt.Sprintf("No provider for category: \"%s\" is found for application: %s", category, application.Name))
return nil, nil, false
}
return provider, user, true
}

View File

@@ -15,6 +15,7 @@
package controllers
import (
"errors"
"fmt"
"strings"
@@ -62,8 +63,8 @@ func (c *ApiController) SendVerificationCode() {
user := c.getCurrentUser()
organization := object.GetOrganization(orgId)
application := object.GetApplicationByOrganizationName(organization.Name)
msg := "Invalid dest type."
sendResp := errors.New("Invalid dest type.")
switch destType {
case "email":
if !util.IsEmailValid(dest) {
@@ -72,7 +73,7 @@ func (c *ApiController) SendVerificationCode() {
}
provider := application.GetEmailProvider()
msg = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone":
if !util.IsPhoneCnValid(dest) {
c.ResponseError("Invalid phone number")
@@ -86,15 +87,15 @@ func (c *ApiController) SendVerificationCode() {
dest = fmt.Sprintf("+%s%s", org.PhonePrefix, dest)
provider := application.GetSmsProvider()
msg = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest)
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest)
}
status := "ok"
if msg != "" {
if sendResp != nil {
status = "error"
}
c.Data["json"] = Response{Status: status, Msg: msg}
c.Data["json"] = Response{Status: status, Msg: sendResp.Error()}
c.ServeJSON()
}

5
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.3.1
github.com/casdoor/go-sms-sender v0.0.3
github.com/casdoor/go-sms-sender v0.0.4
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
@@ -18,6 +18,7 @@ require (
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/mileusna/crontab v1.0.1
github.com/plutov/paypal/v4 v4.3.7
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
github.com/satori/go.uuid v1.2.0 // indirect
@@ -30,5 +31,5 @@ require (
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
xorm.io/core v0.7.2
xorm.io/xorm v1.0.3
xorm.io/xorm v1.0.6
)

10
go.sum
View File

@@ -75,8 +75,8 @@ github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeK
github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/xorm-adapter/v2 v2.3.1 h1:RVGsM6KYFP9s4OQJXrP/gv56Wmt5P40mzvcyXgv5xeg=
github.com/casbin/xorm-adapter/v2 v2.3.1/go.mod h1:GZ+nlIdasVFunQ71SlvkL/HcQQBvFncphDf+2Yl167c=
github.com/casdoor/go-sms-sender v0.0.3 h1:17/dzAP/ZgSY4AORzcsR/48AKyBycQcHUGg00R9tnSI=
github.com/casdoor/go-sms-sender v0.0.3/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
github.com/casdoor/go-sms-sender v0.0.4 h1:UekC70YueeA5E2LrKJVQKCGntdTlYwal/7og4vao66U=
github.com/casdoor/go-sms-sender v0.0.4/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -256,6 +256,8 @@ github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHu
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/plutov/paypal/v4 v4.3.7 h1:wPvhAJ3RkDkV+UDrGX/UivXAl5JEPOOJuzsdgnTMJHc=
github.com/plutov/paypal/v4 v4.3.7/go.mod h1:D56boafCRGcF/fEM0w282kj0fCDKIyrwOPX/Te1jCmw=
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=
@@ -300,6 +302,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
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.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
@@ -622,5 +625,6 @@ xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/xorm v1.0.3 h1:3dALAohvINu2mfEix5a5x5ZmSVGSljinoSGgvGbaZp0=
xorm.io/xorm v1.0.3/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
xorm.io/xorm v1.0.6 h1:7eco1c8QUpGz+3dztpLDj9gU1bTiQdFC/KtmPaLxUJk=
xorm.io/xorm v1.0.6/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=

230
idp/gitlab.go Normal file
View File

@@ -0,0 +1,230 @@
// 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 idp
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
"golang.org/x/oauth2"
)
type GitlabIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewGitlabIdProvider(clientId string, clientSecret string, redirectUrl string) *GitlabIdProvider {
idp := &GitlabIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
func (idp *GitlabIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *GitlabIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
TokenURL: "https://gitlab.com/oauth/token",
}
var config = &oauth2.Config{
Scopes: []string{"read_user+profile"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type GitlabProviderToken struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
CreatedAt int `json:"created_at"`
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://docs.gitlab.com/ee/api/oauth2.html
func (idp *GitlabIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := url.Values{}
params.Add("grant_type", "authorization_code")
params.Add("client_id", idp.Config.ClientID)
params.Add("client_secret", idp.Config.ClientSecret)
params.Add("code", code)
params.Add("redirect_uri", idp.Config.RedirectURL)
accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode())
resp, err := idp.Client.Post(accessTokenUrl, "application/json;charset=UTF-8", nil)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
gtoken := &GitlabProviderToken{}
if err = json.Unmarshal(data, gtoken); err != nil {
return nil, err
}
// gtoken.ExpiresIn always returns 0, so we set Expiry=7200 to avoid verification errors.
token := &oauth2.Token{
AccessToken: gtoken.AccessToken,
TokenType: gtoken.TokenType,
RefreshToken: gtoken.RefreshToken,
Expiry: time.Unix(time.Now().Unix()+int64(7200), 0),
}
return token, nil
}
/*
{
"id":5162115,
"name":"shiluo",
"username":"shiluo",
"state":"active",
"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5162115/avatar.png",
"web_url":"https://gitlab.com/shiluo",
"created_at":"2019-12-23T02:50:10.348Z",
"bio":"",
"bio_html":"",
"location":"China",
"public_email":"silo1999@163.com",
"skype":"",
"linkedin":"",
"twitter":"",
"website_url":"",
"organization":"",
"job_title":"",
"pronouns":null,
"bot":false,
"work_information":null,
"followers":0,
"following":0,
"last_sign_in_at":"2019-12-26T13:24:42.941Z",
"confirmed_at":"2019-12-23T02:52:10.778Z",
"last_activity_on":"2021-08-19",
"email":"silo1999@163.com",
"theme_id":1,
"color_scheme_id":1,
"projects_limit":100000,
"current_sign_in_at":"2021-08-19T09:46:46.004Z",
"identities":[
{
"provider":"github",
"extern_uid":"51157931",
"saml_provider_id":null
}
],
"can_create_group":true,
"can_create_project":true,
"two_factor_enabled":false,
"external":false,
"private_profile":false,
"commit_email":"silo1999@163.com",
"shared_runners_minutes_limit":null,
"extra_shared_runners_minutes_limit":null
}
*/
type GitlabUserInfo struct {
Id int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
State string `json:"state"`
AvatarUrl string `json:"avatar_url"`
WebUrl string `json:"web_url"`
CreatedAt time.Time `json:"created_at"`
Bio string `json:"bio"`
BioHtml string `json:"bio_html"`
Location string `json:"location"`
PublicEmail string `json:"public_email"`
Skype string `json:"skype"`
Linkedin string `json:"linkedin"`
Twitter string `json:"twitter"`
WebsiteUrl string `json:"website_url"`
Organization string `json:"organization"`
JobTitle string `json:"job_title"`
Pronouns interface{} `json:"pronouns"`
Bot bool `json:"bot"`
WorkInformation interface{} `json:"work_information"`
Followers int `json:"followers"`
Following int `json:"following"`
LastSignInAt time.Time `json:"last_sign_in_at"`
ConfirmedAt time.Time `json:"confirmed_at"`
LastActivityOn string `json:"last_activity_on"`
Email string `json:"email"`
ThemeId int `json:"theme_id"`
ColorSchemeId int `json:"color_scheme_id"`
ProjectsLimit int `json:"projects_limit"`
CurrentSignInAt time.Time `json:"current_sign_in_at"`
Identities []struct {
Provider string `json:"provider"`
ExternUid string `json:"extern_uid"`
SamlProviderId interface{} `json:"saml_provider_id"`
} `json:"identities"`
CanCreateGroup bool `json:"can_create_group"`
CanCreateProject bool `json:"can_create_project"`
TwoFactorEnabled bool `json:"two_factor_enabled"`
External bool `json:"external"`
PrivateProfile bool `json:"private_profile"`
CommitEmail string `json:"commit_email"`
SharedRunnersMinutesLimit interface{} `json:"shared_runners_minutes_limit"`
ExtraSharedRunnersMinutesLimit interface{} `json:"extra_shared_runners_minutes_limit"`
}
// GetUserInfo use GitlabProviderToken gotten before return GitlabUserInfo
func (idp *GitlabIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
resp, err := idp.Client.Get("https://gitlab.com/api/v4/user?access_token="+token.AccessToken)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
guser := GitlabUserInfo{}
if err = json.Unmarshal(data, &guser);err != nil {
return nil, err
}
userInfo := UserInfo{
Id: strconv.Itoa(guser.Id),
Username: guser.Username,
DisplayName: guser.Name,
AvatarUrl: guser.AvatarUrl,
Email: guser.Email,
}
return &userInfo, nil
}

View File

@@ -57,6 +57,8 @@ func GetIdProvider(providerType string, clientId string, clientSecret string, re
return NewWeComIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Lark" {
return NewLarkIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "GitLab" {
return NewGitlabIdProvider(clientId, clientSecret, redirectUrl)
}
return nil

View File

@@ -20,8 +20,8 @@ import (
"github.com/astaxie/beego/plugins/cors"
_ "github.com/astaxie/beego/session/redis"
"github.com/casbin/casdoor/authz"
"github.com/casbin/casdoor/controllers"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/proxy"
"github.com/casbin/casdoor/routers"
_ "github.com/casbin/casdoor/routers"
@@ -30,7 +30,8 @@ import (
func main() {
object.InitAdapter()
object.InitDb()
controllers.InitHttpClient()
object.InitDefaultStorageProvider()
proxy.InitHttpClient()
authz.InitAuthz()
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{

View File

@@ -147,4 +147,9 @@ func (a *Adapter) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Payment))
if err != nil {
panic(err)
}
}

View File

@@ -127,6 +127,21 @@ func GetApplicationByUser(user *User) *Application {
}
}
func GetApplicationByUserId(userId string) (*Application, *User) {
var application *Application
owner, name := util.GetOwnerAndNameFromId(userId)
if owner == "app" {
application = getApplication("admin", name)
return application, nil
}
user := GetUser(userId)
application = GetApplicationByUser(user)
return application, user
}
func GetApplicationByClientId(clientId string) *Application {
application := Application{}
existed, err := adapter.Engine.Where("client_id=?", clientId).Get(&application)

View File

@@ -14,7 +14,7 @@
package object
func (application *Application) getProviderByCategory(category string) *Provider {
func (application *Application) GetProviderByCategory(category string) *Provider {
providers := GetProviders(application.Owner)
m := map[string]*Provider{}
for _, provider := range providers {
@@ -35,15 +35,19 @@ func (application *Application) getProviderByCategory(category string) *Provider
}
func (application *Application) GetEmailProvider() *Provider {
return application.getProviderByCategory("Email")
return application.GetProviderByCategory("Email")
}
func (application *Application) GetSmsProvider() *Provider {
return application.getProviderByCategory("SMS")
return application.GetProviderByCategory("SMS")
}
func (application *Application) GetStorageProvider() *Provider {
return application.getProviderByCategory("Storage")
return application.GetProviderByCategory("Storage")
}
func (application *Application) GetPayProvider() *Provider {
return application.GetProviderByCategory("Pay")
}
func (application *Application) getSignupItem(itemName string) *SignupItem {

72
object/avatar.go Normal file
View File

@@ -0,0 +1,72 @@
// 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 (
"bytes"
"fmt"
"io"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/proxy"
)
var defaultStorageProvider *Provider = nil
func InitDefaultStorageProvider() {
defaultStorageProviderStr := beego.AppConfig.String("defaultStorageProvider")
if defaultStorageProviderStr != "" {
defaultStorageProvider = getProvider("admin", defaultStorageProviderStr)
}
}
func downloadFile(url string) (*bytes.Buffer, error) {
httpClient := proxy.GetHttpClient(url)
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
fileBuffer := bytes.NewBuffer(nil)
_, err = io.Copy(fileBuffer, resp.Body)
if err != nil {
return nil, err
}
return fileBuffer, nil
}
func getPermanentAvatarUrl(organization string, username string, url string) string {
if defaultStorageProvider == nil {
return ""
}
fullFilePath := fmt.Sprintf("/avatar/%s/%s.png", organization, username)
uploadedFileUrl, _ := getUploadFileUrl(defaultStorageProvider, fullFilePath, false)
fileBuffer, err := downloadFile(url)
if err != nil {
panic(err)
}
_, _, err = UploadFile(defaultStorageProvider, fullFilePath, fileBuffer)
if err != nil {
panic(err)
}
return uploadedFileUrl
}

View File

@@ -12,23 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
package object
import (
"net/url"
"strings"
"fmt"
"testing"
"github.com/casbin/casdoor/proxy"
)
func parseQuery(query string, key string) string {
queryMap, err := url.ParseQuery(query)
if err != nil {
panic(err)
func TestSyncPermanentAvatars(t *testing.T) {
InitConfig()
InitDefaultStorageProvider()
proxy.InitHttpClient()
users := GetGlobalUsers()
for i, user := range users {
if user.Avatar == "" {
continue
}
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
updateUserColumn("permanent_avatar", user)
fmt.Printf("[%d/%d]: Update user: [%s]'s permanent avatar: %s\n", i, len(users), user.GetId(), user.PermanentAvatar)
}
return queryMap.Get(key)
}
func parseSlash(s string) (string, string) {
tokens := strings.Split(s, "/")
return tokens[0], tokens[1]
}

View File

@@ -83,19 +83,20 @@ func CheckUserSignup(application *Application, organization *Organization, usern
func CheckPassword(user *User, password string) string {
organization := GetOrganizationByUser(user)
if organization == nil {
return "organization does not exist"
}
if organization.PasswordType == "plain" {
if password == user.Password {
return ""
} else {
return "password incorrect"
}
return "password incorrect"
} else if organization.PasswordType == "salt" {
if password == user.Password || getSaltedPassword(password, organization.PasswordSalt) == user.Password {
return ""
} else {
return "password incorrect"
}
return "password incorrect"
} else {
return fmt.Sprintf("unsupported password type: %s", organization.PasswordType)
}

View File

@@ -18,7 +18,7 @@ package object
import "github.com/go-gomail/gomail"
func SendEmail(provider *Provider, title, content, dest, sender string) string {
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
message := gomail.NewMessage()
@@ -27,10 +27,5 @@ func SendEmail(provider *Provider, title, content, dest, sender string) string {
message.SetHeader("Subject", title)
message.SetBody("text/html", content)
err := dialer.DialAndSend(message)
if err == nil {
return ""
} else {
return err.Error()
}
return dialer.DialAndSend(message)
}

View File

@@ -63,6 +63,7 @@ func initBuiltInUser() {
Address: []string{},
Affiliation: "Example Inc.",
Tag: "staff",
Score: 2000,
IsAdmin: true,
IsGlobalAdmin: true,
IsForbidden: false,

View File

@@ -305,6 +305,7 @@ func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]Ldap
Address: []string{user.Address},
Affiliation: "Example Inc.",
Tag: "staff",
Score: 2000,
Ldap: user.Uuid,
}) {
failedUsers = append(failedUsers, user)

View File

@@ -56,9 +56,9 @@ func getOrganization(owner string, name string) *Organization {
if existed {
return &organization
} else {
return nil
}
return nil
}
func GetOrganization(id string) *Organization {

8
object/pay_item.go Normal file
View File

@@ -0,0 +1,8 @@
package object
type PayItem struct {
Invoice string `json:"invoice"`
Price string `json:"price"`
Description string `json:"description"`
Currency string `json:"currency"`
}

74
object/payment.go Normal file
View File

@@ -0,0 +1,74 @@
package object
import (
"github.com/plutov/paypal/v4"
"xorm.io/core"
)
type Payment struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Invoice string `xorm:"varchar(100)" json:"invoice"`
Application string `xorm:"varchar(100)" json:"application"`
PayItem PayItem `xorm:"json varchar(1000)" json:"pay_item"`
Payer *paypal.PayerWithNameAndPhone `xorm:"json varchar(1000)" json:"payer"`
Purchase []paypal.CapturedPurchaseUnit `xorm:"varchar(10000)" json:"purchase"`
Status string `xorm:"varchar(100)" json:"status"`
CreateTime string `xorm:"varchar(100) created" json:"create_time"`
UpdateTime string `xorm:"varchar(100) updated" json:"update_time"`
Callback string `xorm:"varchar(1000)" json:"callback"`
}
func AddPayment(pay *Payment) bool {
affected, err := adapter.Engine.Insert(pay)
if err != nil {
panic(err)
}
return affected != 0
}
func GetPayments() []*Payment {
pays := []*Payment{}
err := adapter.Engine.Desc("create_time").Find(&pays)
if err != nil {
panic(err)
}
return pays
}
func GetPayment(id string) *Payment {
pay := Payment{Id: id}
existed, err := adapter.Engine.Get(&pay)
if err != nil {
panic(err)
}
if existed {
return &pay
} else {
return nil
}
}
func DeletePayment(payment *Payment) bool {
affected, err := adapter.Engine.ID(core.PK{payment.Id}).Delete(&Payment{})
if err != nil {
panic(err)
}
return affected != 0
}
func UpdatePay(id string, pay *Payment) bool {
if GetPayment(id) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{id}).AllCols().Update(pay)
if err != nil {
panic(err)
}
return affected != 0
}

View File

@@ -29,6 +29,7 @@ type Provider struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@@ -57,6 +58,7 @@ func getMaskedProvider(provider *Provider) *Provider {
DisplayName: provider.DisplayName,
Category: provider.Category,
Type: provider.Type,
Method: provider.Method,
ClientId: provider.ClientId,
}
return p

View File

@@ -16,9 +16,9 @@ package object
import (
"fmt"
"xorm.io/core"
"github.com/casbin/casdoor/util"
"xorm.io/core"
)
type Resource struct {
@@ -26,19 +26,26 @@ type Resource struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Provider string `xorm:"varchar(100)" json:"provider"`
Tag string `xorm:"varchar(100)" json:"tag"`
Parent string `xorm:"varchar(100)" json:"parent"`
FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"`
Url string `xorm:"varchar(100)" json:"url"`
ObjectKey string `xorm:"varchar(100)" json:"objectKey"`
User string `xorm:"varchar(100)" json:"user"`
Provider string `xorm:"varchar(100)" json:"provider"`
Application string `xorm:"varchar(100)" json:"application"`
Tag string `xorm:"varchar(100)" json:"tag"`
Parent string `xorm:"varchar(100)" json:"parent"`
FileName string `xorm:"varchar(100)" json:"fileName"`
FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"`
Url string `xorm:"varchar(100)" json:"url"`
}
func GetResources(owner string) []*Resource {
func GetResources(owner string, user string) []*Resource {
if owner == "built-in" {
owner = ""
user = ""
}
resources := []*Resource{}
err := adapter.Engine.Desc("created_time").Find(&resources, &Resource{Owner: owner})
err := adapter.Engine.Desc("created_time").Find(&resources, &Resource{Owner: owner, User: user})
if err != nil {
panic(err)
}
@@ -55,18 +62,18 @@ func getResource(owner string, name string) *Resource {
if existed {
return &resource
} else {
return nil
}
return nil
}
func GetResource(id string) *Resource {
owner, name := util.GetOwnerAndNameFromId(id)
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
return getResource(owner, name)
}
func UpdateResource(id string, resource *Resource) bool {
owner, name := util.GetOwnerAndNameFromId(id)
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
if getResource(owner, name) == nil {
return false
}

View File

@@ -14,24 +14,21 @@
package object
import (
"fmt"
import "github.com/casdoor/go-sms-sender"
"github.com/casdoor/go-sms-sender"
)
func SendCodeToPhone(provider *Provider, phone, code string) string {
client := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.RegionId, provider.TemplateCode, provider.AppId)
if client == nil {
return fmt.Sprintf("Unsupported provide type: %s", provider.Type)
func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
client, err := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.RegionId, provider.TemplateCode, provider.AppId)
if err != nil {
return err
}
param := make(map[string]string)
params := map[string]string{}
if provider.Type == go_sms_sender.TencentCloud {
param["0"] = code
params["0"] = content
} else {
param["code"] = code
params["code"] = content
}
client.SendMessage(param, phone)
return ""
err = client.SendMessage(params, phoneNumbers...)
return err
}

View File

@@ -23,22 +23,8 @@ import (
"github.com/casbin/casdoor/util"
)
func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) {
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint)
if storageProvider == nil {
return "", "", fmt.Errorf("the provider type: %s is not supported", provider.Type)
}
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath)
_, err := storageProvider.Put(objectKey, fileBuffer)
if err != nil {
return "", "", err
}
host := ""
if provider.Type != "Local File System" {
@@ -52,7 +38,32 @@ func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
host = util.UrlJoin(provider.Domain, "/files")
}
fileUrl := fmt.Sprintf("%s?time=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime())
fileUrl := util.UrlJoin(host, objectKey)
if hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime())
}
return fileUrl, objectKey
}
func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) {
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint)
if storageProvider == nil {
return "", "", fmt.Errorf("the provider type: %s is not supported", provider.Type)
}
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
}
fileUrl, objectKey := getUploadFileUrl(provider, fullFilePath, true)
_, err := storageProvider.Put(objectKey, fileBuffer)
if err != nil {
return "", "", err
}
return fileUrl, objectKey, nil
}

View File

@@ -72,9 +72,9 @@ func getToken(owner string, name string) *Token {
if existed {
return &token
} else {
return nil
}
return nil
}
func getTokenByCode(code string) *Token {
@@ -86,9 +86,9 @@ func getTokenByCode(code string) *Token {
if existed {
return &token
} else {
return nil
}
return nil
}
func GetToken(id string) *Token {

View File

@@ -32,14 +32,21 @@ type User struct {
Password string `xorm:"varchar(100)" json:"password"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(255)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(255)" json:"permanentAvatar"`
Email string `xorm:"varchar(100)" json:"email"`
Phone string `xorm:"varchar(100)" json:"phone"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
Title string `xorm:"varchar(100)" json:"title"`
Homepage string `xorm:"varchar(100)" json:"homepage"`
Bio string `xorm:"varchar(100)" json:"bio"`
Tag string `xorm:"varchar(100)" json:"tag"`
Region string `xorm:"varchar(100)" json:"region"`
Language string `xorm:"varchar(100)" json:"language"`
Score int `json:"score"`
Ranking int `json:"ranking"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsGlobalAdmin bool `json:"isGlobalAdmin"`
IsForbidden bool `json:"isForbidden"`
@@ -58,6 +65,7 @@ type User struct {
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
Lark string `xorm:"lark varchar(100)" json:"lark"`
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
@@ -140,14 +148,19 @@ func GetLastUser(owner string) *User {
func UpdateUser(id string, user *User) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getUser(owner, name) == nil {
oldUser := getUser(owner, name)
if oldUser == nil {
return false
}
user.UpdateUserHash()
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar",
"address", "region", "language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "is_admin", "is_global_admin", "is_forbidden",
"hash", "properties").Update(user)
if err != nil {
panic(err)
@@ -156,14 +169,19 @@ func UpdateUser(id string, user *User) bool {
return affected != 0
}
func UpdateUserInternal(id string, user *User) bool {
func UpdateUserForAllFields(id string, user *User) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getUser(owner, name) == nil {
oldUser := getUser(owner, name)
if oldUser == nil {
return false
}
user.UpdateUserHash()
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
if err != nil {
panic(err)
@@ -172,7 +190,17 @@ func UpdateUserInternal(id string, user *User) bool {
return affected != 0
}
func UpdateUserForOriginal(user *User) bool {
func UpdateUserForOriginalFields(user *User) bool {
owner, name := util.GetOwnerAndNameFromId(user.GetId())
oldUser := getUser(owner, name)
if oldUser == nil {
return false
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
}
affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols("display_name", "password", "phone", "avatar", "affiliation", "score", "is_forbidden", "hash", "pre_hash").Update(user)
if err != nil {
panic(err)
@@ -192,6 +220,8 @@ func AddUser(user *User) bool {
user.UpdateUserHash()
user.PreHash = user.Hash
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
affected, err := adapter.Engine.Insert(user)
if err != nil {
panic(err)
@@ -211,6 +241,8 @@ func AddUsers(users []*User) bool {
user.UpdateUserHash()
user.PreHash = user.Hash
user.PermanentAvatar = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar)
}
affected, err := adapter.Engine.Insert(users)
@@ -237,7 +269,8 @@ func AddUsersSafe(users []*User) bool {
}
tmp := users[start:end]
fmt.Printf("Add users: [%d - %d].\n", start, end)
// TODO: save to log instead of standard output
// fmt.Printf("Add users: [%d - %d].\n", start, end)
if AddUsers(tmp) {
affected = true
}

View File

@@ -91,8 +91,8 @@ func TestGetMaskedUsers(t *testing.T) {
}{
{
name: "1",
args: args{users: []*User{{Password: "casdoor"},{Password: "casbin"}}},
want: []*User{{Password: "***"},{Password: "***"}},
args: args{users: []*User{{Password: "casdoor"}, {Password: "casbin"}}},
want: []*User{{Password: "***"}, {Password: "***"}},
},
}
for _, tt := range tests {
@@ -102,4 +102,4 @@ func TestGetMaskedUsers(t *testing.T) {
}
})
}
}
}

View File

@@ -135,7 +135,7 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
}
}
affected := UpdateUserInternal(user.GetId(), user)
affected := UpdateUserForAllFields(user.GetId(), user)
return affected
}

View File

@@ -15,6 +15,7 @@
package object
import (
"errors"
"fmt"
"math/rand"
"time"
@@ -39,9 +40,9 @@ type VerificationRecord struct {
IsUsed bool
}
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) string {
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
if provider == nil {
return "Please set an Email provider first"
return fmt.Errorf("Please set an Email provider first")
}
sender := organization.DisplayName
@@ -50,27 +51,27 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code)
if result := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); len(result) != 0 {
return result
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
return err
}
return SendEmail(provider, title, content, dest, sender)
}
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) string {
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
if provider == nil {
return "Please set a SMS provider first"
return errors.New("Please set a SMS provider first")
}
code := getRandomCode(5)
if result := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); len(result) != 0 {
return result
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
return err
}
return SendCodeToPhone(provider, dest, code)
return SendSms(provider, dest, code)
}
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) string {
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) error {
var record VerificationRecord
record.RemoteAddr = remoteAddr
record.Type = recordType
@@ -79,12 +80,12 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
}
has, err := adapter.Engine.Desc("created_time").Get(&record)
if err != nil {
panic(err)
return err
}
now := time.Now().Unix()
if has && now-record.Time < 60 {
return "You can only send one code in 60s."
return errors.New("You can only send one code in 60s.")
}
record.Owner = provider.Owner
@@ -102,10 +103,10 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
_, err = adapter.Engine.Insert(record)
if err != nil {
panic(err)
return err
}
return ""
return nil
}
func getVerificationRecord(dest string) *VerificationRecord {

View File

@@ -112,7 +112,7 @@ func syncUsers() {
updatedUser := createUserFromOriginalUser(oUser, affiliationMap)
updatedUser.Hash = oHash
updatedUser.PreHash = oHash
object.UpdateUserForOriginal(updatedUser)
object.UpdateUserForOriginalFields(updatedUser)
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
}
} else {
@@ -133,7 +133,7 @@ func syncUsers() {
updatedUser := createUserFromOriginalUser(oUser, affiliationMap)
updatedUser.Hash = oHash
updatedUser.PreHash = oHash
object.UpdateUserForOriginal(updatedUser)
object.UpdateUserForOriginalFields(updatedUser)
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
}
}

105
payment/paypal.go Normal file
View File

@@ -0,0 +1,105 @@
package payment
import (
"context"
"fmt"
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/object"
"github.com/plutov/paypal/v4"
)
var client = GetClient()
func GetClient() *paypal.Client {
c, err := paypal.NewClient(beego.AppConfig.String("paypalClientId"), beego.AppConfig.String("paypalSecret"), paypal.APIBaseSandBox)
if err != nil {
panic(err)
}
return c
}
func Paypal(payItem object.PayItem, clientId string, redirectUri string) string {
application := object.GetApplicationByClientId(clientId)
if application == nil {
return "Invalid client_id"
}
applicationName := fmt.Sprintf("%s/%s", application.Owner, application.Name)
if payItem.Currency == "" {
payItem.Currency = "USD"
}
_, err := client.GetAccessToken(context.Background())
if err != nil {
panic(err)
}
appContext := &paypal.ApplicationContext{
ReturnURL: "http://localhost:7001/pay/success", //回调链接
CancelURL: "https://www.baidu.com",
}
purchaseUnits := make([]paypal.PurchaseUnitRequest, 1)
purchaseUnits[0] = paypal.PurchaseUnitRequest{
Amount: &paypal.PurchaseUnitAmount{
Currency: payItem.Currency, //收款类型
Value: payItem.Price, //收款数量
},
InvoiceID: payItem.Invoice,
Description: payItem.Description,
}
order, err := client.CreateOrder(context.Background(),
paypal.OrderIntentCapture,
purchaseUnits,
&paypal.CreateOrderPayer{},
appContext)
if err != nil {
panic(err)
}
newPay := object.Payment{
Id: order.ID,
Invoice: payItem.Invoice,
PayItem: payItem,
Application: applicationName,
Status: order.Status,
Callback: redirectUri,
}
success := object.AddPayment(&newPay)
if success {
links := order.Links
for _, link := range links {
fmt.Println(link.Rel)
if link.Rel == "approve" {
return link.Href
}
}
}
return "Add Order to Database false"
}
func SuccessPay(token string) string {
_, err := client.GetAccessToken(context.Background())
if err != nil {
panic(err)
}
captureOrder, err := client.CaptureOrder(context.Background(), token, paypal.CaptureOrderRequest{})
if err != nil {
panic(err)
}
pay := object.GetPayment(captureOrder.ID)
pay.Purchase = captureOrder.PurchaseUnits
pay.Payer = captureOrder.Payer
pay.UpdateTime = time.Now().String()
pay.Status = captureOrder.Status
object.UpdatePay(captureOrder.ID, pay)
if captureOrder.Status == "COMPLETED" {
return fmt.Sprintf("%s?paymentId=%s", pay.Callback, token)
}
return ""
}

64
proxy/proxy.go Normal file
View File

@@ -0,0 +1,64 @@
// 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 proxy
import (
"net/http"
"strings"
"github.com/astaxie/beego"
"golang.org/x/net/proxy"
)
var DefaultHttpClient *http.Client
var ProxyHttpClient *http.Client
func InitHttpClient() {
// not use proxy
DefaultHttpClient = http.DefaultClient
// use proxy
httpProxy := beego.AppConfig.String("httpProxy")
if httpProxy == "" {
ProxyHttpClient = DefaultHttpClient
return
}
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", httpProxy, nil, proxy.Direct)
if err != nil {
panic(err)
}
tr := &http.Transport{Dial: dialer.Dial}
ProxyHttpClient = &http.Client{
Transport: tr,
}
//resp, err2 := ProxyHttpClient.Get("https://google.com")
//if err2 != nil {
// panic(err2)
//}
//defer resp.Body.Close()
//println("Response status: %s", resp.Status)
}
func GetHttpClient(url string) *http.Client {
if strings.Contains(url, "githubusercontent.com") {
return ProxyHttpClient
} else {
return DefaultHttpClient
}
}

View File

@@ -18,11 +18,9 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/authz"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
@@ -31,20 +29,6 @@ type Object struct {
Name string `json:"name"`
}
func getUsernameByClientIdSecret(ctx *context.Context) string {
clientId := ctx.Input.Query("clientId")
clientSecret := ctx.Input.Query("clientSecret")
if len(clientId) == 0 || len(clientSecret) == 0 {
return ""
}
app := object.GetApplicationByClientId(clientId)
if app == nil || app.ClientSecret != clientSecret {
return ""
}
return "built-in/service"
}
func getUsername(ctx *context.Context) (username string) {
defer func() {
if r := recover(); r != nil {
@@ -52,11 +36,9 @@ func getUsername(ctx *context.Context) (username string) {
}
}()
// bug in Beego: this call will panic when file session store is empty
// so we catch the panic
username = ctx.Input.Session("username").(string)
if len(username) == 0 {
if username == "" {
username = getUsernameByClientIdSecret(ctx)
}
@@ -70,22 +52,19 @@ func getSubject(ctx *context.Context) (string, string) {
}
// username == "built-in/admin"
tokens := strings.Split(username, "/")
owner := tokens[0]
name := tokens[1]
return owner, name
return util.GetOwnerAndNameFromId(username)
}
func getObject(ctx *context.Context) (string, string) {
method := ctx.Request.Method
if method == http.MethodGet {
query := ctx.Request.URL.RawQuery
// query == "?id=built-in/admin"
idParamValue := parseQuery(query, "id")
if idParamValue == "" {
id := ctx.Input.Query("id")
if id == "" {
return "", ""
}
return parseSlash(idParamValue)
return util.GetOwnerAndNameFromId(id)
} else {
body := ctx.Input.RequestBody

View File

@@ -16,60 +16,23 @@ package routers
import (
"fmt"
"net/url"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/controllers"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
func getSessionUser(ctx *context.Context) string {
user := ctx.Input.CruSession.Get("username")
if user == nil {
return ""
}
return user.(string)
}
func setSessionUser(ctx *context.Context, user string) {
err := ctx.Input.CruSession.Set("username", user)
if err != nil {
panic(err)
}
// https://github.com/beego/beego/issues/3445#issuecomment-455411915
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}
func returnRequest(ctx *context.Context, msg string) {
w := ctx.ResponseWriter
w.WriteHeader(200)
resp := &controllers.Response{Status: "error", Msg: msg}
_, err := w.Write([]byte(util.StructToJson(resp)))
if err != nil {
panic(err)
}
}
func AutoSigninFilter(ctx *context.Context) {
//if getSessionUser(ctx) != "" {
// return
//}
query := ctx.Request.URL.RawQuery
queryMap, err := url.ParseQuery(query)
if err != nil {
panic(err)
}
// "/page?access_token=123"
accessToken := queryMap.Get("accessToken")
accessToken := ctx.Input.Query("accessToken")
if accessToken != "" {
claims, err := object.ParseJwtToken(accessToken)
if err != nil {
returnRequest(ctx, "Invalid JWT token")
responseError(ctx, "invalid JWT token")
return
}
@@ -78,14 +41,21 @@ func AutoSigninFilter(ctx *context.Context) {
return
}
// "/page?clientId=123&clientSecret=456"
userId := getUsernameByClientIdSecret(ctx)
if userId != "" {
setSessionUser(ctx, userId)
return
}
// "/page?username=abc&password=123"
userId := queryMap.Get("username")
password := queryMap.Get("password")
userId = ctx.Input.Query("username")
password := ctx.Input.Query("password")
if userId != "" && password != "" {
owner, name := util.GetOwnerAndNameFromId(userId)
_, msg := object.CheckUserPassword(owner, name, password)
if msg != "" {
returnRequest(ctx, msg)
responseError(ctx, msg)
return
}

View File

@@ -14,7 +14,12 @@
package routers
import "github.com/astaxie/beego/context"
import (
"fmt"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object"
)
type Response struct {
Status string `json:"status"`
@@ -42,3 +47,37 @@ func responseError(ctx *context.Context, error string, data ...interface{}) {
func denyRequest(ctx *context.Context) {
responseError(ctx, "Unauthorized operation")
}
func getUsernameByClientIdSecret(ctx *context.Context) string {
clientId := ctx.Input.Query("clientId")
clientSecret := ctx.Input.Query("clientSecret")
if clientId == "" || clientSecret == "" {
return ""
}
application := object.GetApplicationByClientId(clientId)
if application == nil || application.ClientSecret != clientSecret {
return ""
}
return fmt.Sprintf("app/%s", application.Name)
}
func getSessionUser(ctx *context.Context) string {
user := ctx.Input.CruSession.Get("username")
if user == nil {
return ""
}
return user.(string)
}
func setSessionUser(ctx *context.Context, user string) {
err := ctx.Input.CruSession.Set("username", user)
if err != nil {
panic(err)
}
// https://github.com/beego/beego/issues/3445#issuecomment-455411915
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}

View File

@@ -15,7 +15,7 @@
package routers
import (
"strings"
"fmt"
"github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object"
@@ -39,31 +39,31 @@ func getUser(ctx *context.Context) (username string) {
}
func getUserByClientIdSecret(ctx *context.Context) string {
requestUri := ctx.Request.RequestURI
clientId := parseQuery(requestUri, "clientId")
clientSecret := parseQuery(requestUri, "clientSecret")
if len(clientId) == 0 || len(clientSecret) == 0 {
clientId := ctx.Input.Query("clientId")
clientSecret := ctx.Input.Query("clientSecret")
if clientId == "" || clientSecret == "" {
return ""
}
app := object.GetApplicationByClientId(clientId)
if app == nil || app.ClientSecret != clientSecret {
application := object.GetApplicationByClientId(clientId)
if application == nil || application.ClientSecret != clientSecret {
return ""
}
return app.Organization + "/" + app.Name
return fmt.Sprintf("%s/%s", application.Organization, application.Name)
}
func RecordMessage(ctx *context.Context) {
if ctx.Request.URL.Path != "/api/login" {
user := getUser(ctx)
userinfo := strings.Split(user, "/")
if user == "" {
userinfo = append(userinfo, "")
}
record := util.Records(ctx)
record.Organization = userinfo[0]
record.Username = userinfo[1]
object.AddRecord(record)
if ctx.Request.URL.Path == "/api/login" {
return
}
record := util.Records(ctx)
userId := getUser(ctx)
if userId != "" {
record.Organization, record.Username = util.GetOwnerAndNameFromId(userId)
}
object.AddRecord(record)
}

View File

@@ -49,6 +49,7 @@ func initAPI() {
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
beego.Router("/api/get-application-clientId", &controllers.ApiController{}, "Get:GetApplicationByClientId")
beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization")
beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization")
beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization")
@@ -108,4 +109,9 @@ func initAPI() {
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/api/paypal", &controllers.ApiController{}, "POST:PaypalPay")
beego.Router("/api/success-pay", &controllers.ApiController{}, "GET:SuccessPay")
beego.Router("/api/get-payments", &controllers.ApiController{}, "Get:GetPayments")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment")
}

View File

@@ -43,6 +43,11 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1]
}
func GetOwnerAndNameFromIdNoCheck(id string) (string, string) {
tokens := strings.SplitN(id, "/", 2)
return tokens[0], tokens[1]
}
func GenerateId() string {
return uuid.NewString()
}

View File

@@ -50,6 +50,9 @@ import AuthCallback from "./auth/AuthCallback";
import SelectLanguageBox from './SelectLanguageBox';
import i18next from 'i18next';
import PromptPage from "./auth/PromptPage";
import PaySuccessCallback from "./PaySuccessCallback";
import Pay from "./Pay";
import PaymentListPage from "./PaymentListPage";
const { Header, Footer } = Layout;
@@ -72,7 +75,6 @@ class App extends Component {
}
UNSAFE_componentWillMount() {
Setting.setLanguage();
this.updateMenuKey();
this.getAccount();
}
@@ -113,6 +115,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/login' });
} else if (uri.includes('/result')) {
this.setState({ selectedMenuKey: '/result' });
}else if (uri.includes('/payment')) {
this.setState({ selectedMenuKey: '/payment' });
} else {
this.setState({ selectedMenuKey: -1 });
}
@@ -139,6 +143,13 @@ class App extends Component {
return location.toString().replace(location.search, "");
}
setLanguage(account) {
let language = account?.language;
if (language !== "" && language !== i18next.language) {
Setting.setLanguage(language);
}
}
getAccount() {
let query = this.getAccessTokenParam();
if (query === "") {
@@ -153,6 +164,8 @@ class App extends Component {
if (res.status === "ok") {
account = res.data;
account.organization = res.data2;
this.setLanguage(account);
} else {
if (res.msg !== "Please sign in first") {
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
@@ -194,9 +207,9 @@ class App extends Component {
}
handleRightDropdownClick(e) {
if (e.key === '201') {
if (e.key === '/account') {
this.props.history.push(`/account`);
} else if (e.key === '202') {
} else if (e.key === 'logout') {
this.logout();
}
}
@@ -220,11 +233,11 @@ class App extends Component {
renderRightDropdown() {
const menu = (
<Menu onClick={this.handleRightDropdownClick.bind(this)}>
<Menu.Item key="201">
<Menu.Item key="/account">
<SettingOutlined />
{i18next.t("account:My Account")}
</Menu.Item>
<Menu.Item key="202">
<Menu.Item key="/logout">
<LogoutOutlined />
{i18next.t("account:Logout")}
</Menu.Item>
@@ -232,7 +245,7 @@ class App extends Component {
);
return (
<Dropdown key="200" overlay={menu} className="rightDropDown">
<Dropdown key="/rightDropDown" overlay={menu} className="rightDropDown">
<div className="ant-dropdown-link" style={{float: 'right', cursor: 'pointer'}}>
&nbsp;
&nbsp;
@@ -321,13 +334,17 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/resources">
<Link to="/resources">
{i18next.t("general:Resources")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/resources">
<Link to="/resources">
{i18next.t("general:Resources")}
</Link>
</Menu.Item>
);
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/tokens">
<Link to="/tokens">
@@ -336,20 +353,29 @@ class App extends Component {
</Menu.Item>
);
res.push(
<Menu.Item key="/records">
<Link to="/records">
{i18next.t("general:Records")}
<Menu.Item key="/records">
<Link to="/records">
{i18next.t("general:Records")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/payment">
<Link to="/payment">
{"Payment"}
</Link>
</Menu.Item>
);
)
}
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={"/swagger"}>
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
{i18next.t("general:Swagger")}
</a>
</Menu.Item>
);
return res;
}
@@ -402,6 +428,9 @@ class App extends Component {
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)}/>
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/payment" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/pay" render={(props) => <Pay account={this.state.account} {...props}/>}/>
<Route exact path="/pay/success" render={(props) => <PaySuccessCallback account={this.state.account} {...props}/>}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
@@ -414,31 +443,35 @@ class App extends Component {
return (
<div style={{display: 'flex', flex: 'auto',width:"100%",flexDirection: 'column'}}>
<Layout style={{display: 'flex', alignItems: 'stretch'}}>
<Header style={{ padding: '0', marginBottom: '3px'}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
}
<Menu
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{ lineHeight: '64px'}}
>
{window.location.pathname.indexOf("/pay") != -1 && window.location.pathname.indexOf("/payment") == -1 ?
null :
(<Header style={{ padding: '0', marginBottom: '3px'}}>
{
this.renderMenu()
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
}
<div style = {{float: 'right'}}>
{
this.renderAccount()
}
<SelectLanguageBox/>
</div>
</Menu>
</Header>
<Menu
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{ lineHeight: '64px'}}
>
{
this.renderMenu()
}
<div style = {{float: 'right'}}>
{
this.renderAccount()
}
<SelectLanguageBox/>
</div>
</Menu>
</Header>)
}
<Layout style={{backgroundColor: "#f5f5f5", alignItems: 'stretch'}}>
<Card className="content-warp-card">
{
@@ -452,31 +485,34 @@ class App extends Component {
} else {
return(
<div>
<Header style={{ padding: '0', marginBottom: '3px'}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
{window.location.pathname.indexOf("/pay") != -1 && window.location.pathname.indexOf("/payment") == -1 ?
null :
<Header style={{ padding: '0', marginBottom: '3px'}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
}
<Menu
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{ lineHeight: '64px' }}
>
{
this.renderMenu()
}
<div style = {{float: 'right'}}>
{
this.renderAccount()
}
<SelectLanguageBox/>
</div>
</Menu>
</Header>
}
<Menu
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{ lineHeight: '64px' }}
>
{
this.renderMenu()
}
<div style = {{float: 'right'}}>
{
this.renderAccount()
}
<SelectLanguageBox/>
</div>
</Menu>
</Header>
{
this.renderRouter()
}

View File

@@ -99,7 +99,7 @@ class ApplicationEditPage extends React.Component {
}
this.setState({uploading: true});
const fullFilePath = `termsOfUse/${this.state.application.owner}/${this.state.application.name}.html`;
ResourceBackend.uploadResource(this.state.application.owner, "termsOfUse", this.state.application.name, fullFilePath, info.file)
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "termsOfUse", "ApplicationEditPage", fullFilePath, info.file)
.then(res => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export const ShowGithubCorner = false
export const ShowGithubCorner = false;
export const GithubRepo = "https://github.com/casbin/casdoor";
export const GithubRepo = "https://github.com/casbin/casdoor"
export const ForceLanguage = "en";
export const DefaultLanguage = "en";

View File

@@ -58,7 +58,7 @@ export const CropperDiv = (props) => {
// Setting.showMessage("success", "uploading...");
const extension = image.substring(image.indexOf('/') + 1, image.indexOf(';base64'));
const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`;
ResourceBackend.uploadResource("admin", "avatar", account.name, fullFilePath, blob)
ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDiv", fullFilePath, blob)
.then((res) => {
if (res.status === "ok") {
window.location.href = "/account";

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import * as Conf from "./common/Conf"
import * as Conf from "./Conf";
import GithubCorner from "react-github-corner";
class CustomGithubCorner extends React.Component {

176
web/src/Pay.js Normal file
View File

@@ -0,0 +1,176 @@
// 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, Form, Input,} from "antd";
import {withRouter} from "react-router-dom";
import * as PaymentBackend from "./backend/PaymentBackend"
import * as ApplicationBackend from "./backend/ApplicationBackend"
class Pay extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: null,
classes: props,
clientId: this.GetQueryString("clientId"),
invoice: this.GetQueryString("invoice"),
price: this.GetQueryString("price"),
currency: this.GetQueryString("currency"),
description: this.GetQueryString("description"),
redirectUri: this.GetQueryString("redirectUri"),
waiting: false,
applicationName: "",
wrongMsg: "",
spinMsg: "正在生成订单..."
};
}
UNSAFE_componentWillMount() {
this.getApplication();
}
getApplication(){
ApplicationBackend.getApplicationByClientId(this.state.clientId).then(res => {
if(res == null){
this.setState({
wrongMsg: "Invalid clientID"
})
}else {
console.log("res")
console.log(res)
this.setState({
applicationName : res.displayName
})
}
})
}
GetQueryString(key){
let reg = new RegExp("(^|&)"+ key +"=([^&]*)(&|$)");
let r = window.location.search.substr(1).match(reg);
if(r!=null)return unescape(r[2]); return null;
}
getCurrencyString(currency){
if(currency === "USD"){
return "$"
}else if (currency === "CNY"){
return "¥"
}else if (currency === "EUR"){
return "€"
}
return "$"
}
pay(){
let payItem = {
invoice: this.state.invoice,
price : this.state.price,
currency : this.state.currency,
description : this.state.description
}
this.setState({
waiting: true
})
PaymentBackend.PaypalPal(payItem, this.state.clientId, this.state.redirectUri).then(res => {
console.log(res)
if (res.indexOf("http") !== -1){
this.setState({
spinMsg: "前往支付页面"
})
window.location.replace(res);
}
else {
this.setState({
wrongMsg : res
})
}
})
}
render() {
const data = [
{
title: 'Invoice',
},
{
title: 'Amount',
},
{
title: 'Description',
}
];
return (
<div>
{
(this.state.wrongMsg === "") ?
(<div style={{textAlign: "center"}}>
{
(this.state.waiting) ? (
<Spin size="large" tip={this.state.spinMsg} style={{paddingTop: "10%"}} />
) : (
<div style={{display: "inline"}}>
<p style={{fontSize: 20}}>{`尊敬的用户,您正在通过 casdoor 向 ${this.state.applicationName} 进行付款,请确认`}</p>
<Form
size="large"
name="pay"
labelCol={{ span: 8 }}
wrapperCol={{ span: 8 }}
initialValues={{ remember: true }}
autoComplete="off"
>
<Form.Item
label="Invoice"
>
<Input style={{color: "black"}} value={this.state.invoice} bordered={false} disabled />
</Form.Item>
<Form.Item
label="Account"
>
<Input style={{color: "black"}} disabled bordered={false} value={`${this.getCurrencyString(this.state.currency)} ${this.state.price}`}/>
</Form.Item>
<Form.Item
label="Description"
>
<Input style={{color: "black"}} bordered={false} disabled value={`${this.state.description}`}/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 8 }}>
<Button size="large" type="primary" onClick={() => this.pay()}>
确认付款
</Button>
</Form.Item>
</Form>
</div>
)
}
</div>) :
( <div style={{display: "inline"}}>
<Result
status="error"
title="Pay Error"
subTitle={this.state.wrongMsg}
/>
</div>)
}
</div>
);
}
}
export default withRouter(Pay);

View File

@@ -0,0 +1,71 @@
// 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 PaymentBackend from "./backend/PaymentBackend"
class PaySuccessCallback extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
msg: null,
paymentId: this.GetQueryString("token"),
payerId: this.GetQueryString("PayerID")
};
}
// eslint-disable-next-line react/no-deprecated
componentWillMount() {
PaymentBackend.PaySuccess(this.state.paymentId, this.state.payerId).then(res => {
window.location.href = res
})
}
GetQueryString(key){
let reg = new RegExp("(^|&)"+ key +"=([^&]*)(&|$)");
let r = window.location.search.substr(1).match(reg);
if(r!=null)return unescape(r[2]); return null;
}
render() {
return (
<div style={{textAlign: "center"}}>
{
(this.state.msg === null) ? (
<Spin size="large" tip="支付完成,正在跳转..." 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(PaySuccessCallback);

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

@@ -0,0 +1,208 @@
// 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, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";
import i18next from "i18next";
class PaymentListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
payments: [],
};
}
UNSAFE_componentWillMount() {
this.getPayments();
}
getPayments() {
PaymentBackend.getPayments().then(res => {
this.setState({
payments: res
})
})
}
deletePayment(i) {
PaymentBackend.deletePayment(this.state.payments[i]).then(res => {
Setting.showMessage("success", `Payment deleted successfully`);
this.setState({
payments: Setting.deleteRow(this.state.payments, i),
});
}).catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`);
});
}
renderTable(payments) {
const columns = [
{
title: "Id",
dataIndex: 'id',
key: 'id',
width: (Setting.isMobile()) ? "100px" : "170px",
fixed: 'left',
sorter: (a, b) => a.id.localeCompare(b.id),
},
{
title: "invoice",
dataIndex: 'invoice',
key: 'invoice',
width: '150px',
sorter: (a, b) => a.invoice.localeCompare(b.invoice),
},
{
title: "Application",
dataIndex: 'application',
key: 'application',
width: '120px',
sorter: (a, b) => a.application.localeCompare(b.application),
render: (text, record, index) => {
return (
<Link to={`/applications/${text.substring(text.indexOf("/")+1)}`}>
{text}
</Link>
)
}
},
{
title: "Amount",
dataIndex: 'pay_item',
key: 'pay_item',
width: '150px',
ellipsis: true,
render: (text, record, index) => {
return (
`amount: ${text.currency} ${text.price}`
)
}
},
{
title: "Status",
dataIndex: 'status',
key: 'status',
width: '120px',
sorter: (a, b) => a.status.localeCompare(b.status),
},
{
title: "Payer",
dataIndex: 'payer',
key: 'payer',
width: '120px',
render: (text, record, index) => {
return (
text === null ? "" : (
`${text?.name.surname} ${text?.name.given_name}`
)
)
}
},
{
title: "Payer Email",
dataIndex: 'payer',
key: 'payer',
width: '160px',
ellipsis: true,
render: (text, record, index) => {
return (
text === null ? "" : text.email_address
)
}
},
{
title: "Create_time",
dataIndex: 'create_time',
key: 'create_time',
width: '150px',
sorter: (a, b) => a.create_time.localeCompare(b.create_time),
},
{
title: "update_time",
dataIndex: 'update_time',
key: 'update_time',
width: '150px',
sorter: (a, b) => a.update_time.localeCompare(b.update_time),
},
{
title: "description",
dataIndex: 'pay_item',
key: 'pay_item',
ellipsis: true,
width: '150px',
render: (text, record, index) => {
return (
text.description
)
}
},
{
title: "Callback",
dataIndex: 'callback',
key: 'callback',
width: '100px',
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '80px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Popconfirm
title={`Sure to delete this payment ?`}
onConfirm={() => this.deletePayment(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={payments} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{"Payments"}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
)}
loading={payments === null}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.payments)
}
</div>
);
}
}
export default PaymentListPage;

View File

@@ -77,6 +77,7 @@ class ProviderEditPage extends React.Component {
{id: 'LinkedIn', name: 'LinkedIn'},
{id: 'WeCom', name: 'WeCom'},
{id: 'Lark', name: 'Lark'},
{id: 'GitLab', name: 'GitLab'},
]
);
} else if (provider.category === "Email") {
@@ -101,6 +102,12 @@ class ProviderEditPage extends React.Component {
{id: 'Aliyun OSS', name: 'Aliyun OSS'},
]
);
} else if (provider.category === "Payment") {
return (
[
{id: 'Paypal', name: 'PayPay'}
]
)
} else {
return [];
}
@@ -200,6 +207,8 @@ class ProviderEditPage extends React.Component {
} else if (value === "Storage") {
this.updateProviderField('type', 'Local File System');
this.updateProviderField('domain', Setting.getFullServerUrl());
} else if (value === "Pay") {
this.updateProviderField('type', 'Paypal')
}
})}>
{
@@ -208,6 +217,7 @@ class ProviderEditPage extends React.Component {
{id: 'Email', name: 'Email'},
{id: 'SMS', name: 'SMS'},
{id: 'Storage', name: 'Storage'},
{id: 'Payment', name: 'Payment'}
].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
}
</Select>
@@ -230,6 +240,22 @@ class ProviderEditPage extends React.Component {
</Select>
</Col>
</Row>
{this.state.provider.type === "WeCom" ? (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.provider.method} onChange={value => {
this.updateProviderField('method', value);
}}>
{
[{name: "Normal"}, {name: "Silent"}].map((method, index) => <Option key={index} value={method.name}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
) : null}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel()}

View File

@@ -51,6 +51,7 @@ class ProviderListPage extends React.Component {
displayName: `New Provider - ${this.state.providers.length}`,
category: "OAuth",
type: "GitHub",
method: "Normal",
clientId: "",
clientSecret: "",
enableSignUp: true,

View File

@@ -151,6 +151,7 @@ class ProviderTable extends React.Component {
)
}
},
// {
// title: i18next.t("provider:alertType"),
// dataIndex: 'alertType',

View File

@@ -37,7 +37,7 @@ class ResourceListPage extends React.Component {
}
getResources() {
ResourceBackend.getResources("admin")
ResourceBackend.getResources(this.props.account.owner, this.props.account.name)
.then((res) => {
this.setState({
resources: res,
@@ -63,7 +63,7 @@ class ResourceListPage extends React.Component {
this.setState({uploading: true});
const filename = info.fileList[0].name;
const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`;
ResourceBackend.uploadResource("admin", "custom", this.props.account.name, fullFilePath, info.file)
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "custom", "ResourceListPage", fullFilePath, info.file)
.then(res => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
@@ -104,6 +104,41 @@ class ResourceListPage extends React.Component {
)
}
},
{
title: i18next.t("resource:Application"),
dataIndex: 'application',
key: 'application',
width: '80px',
sorter: (a, b) => a.application.localeCompare(b.application),
render: (text, record, index) => {
return (
<Link to={`/applications/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("resource:User"),
dataIndex: 'user',
key: 'user',
width: '80px',
sorter: (a, b) => a.user.localeCompare(b.user),
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${record.user}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("resource:Parent"),
dataIndex: 'parent',
key: 'parent',
width: '80px',
sorter: (a, b) => a.parent.localeCompare(b.parent),
},
{
title: i18next.t("general:Name"),
dataIndex: 'name',
@@ -115,7 +150,7 @@ class ResourceListPage extends React.Component {
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
width: '150px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
@@ -129,31 +164,31 @@ class ResourceListPage extends React.Component {
sorter: (a, b) => a.tag.localeCompare(b.tag),
},
{
title: i18next.t("resource:Parent"),
dataIndex: 'parent',
key: 'parent',
width: '80px',
sorter: (a, b) => a.parent.localeCompare(b.parent),
title: i18next.t("resource:File name"),
dataIndex: 'fileName',
key: 'fileName',
width: '120px',
sorter: (a, b) => a.fileName.localeCompare(b.fileName),
},
{
title: i18next.t("resource:File type"),
title: i18next.t("resource:Type"),
dataIndex: 'fileType',
key: 'fileType',
width: '120px',
width: '80px',
sorter: (a, b) => a.fileType.localeCompare(b.fileType),
},
{
title: i18next.t("resource:File format"),
title: i18next.t("resource:Format"),
dataIndex: 'fileFormat',
key: 'fileFormat',
width: '130px',
width: '80px',
sorter: (a, b) => a.fileFormat.localeCompare(b.fileFormat),
},
{
title: i18next.t("resource:File size"),
dataIndex: 'fileSize',
key: 'fileSize',
width: '120px',
width: '100px',
sorter: (a, b) => a.fileSize - b.fileSize,
render: (text, record, index) => {
return Setting.getFriendlyFileSize(text);

View File

@@ -30,13 +30,11 @@ class SelectLanguageBox extends React.Component {
};
}
onClick(e) {
Setting.changeLanguage(e.key);
};
render() {
const menu = (
<Menu onClick={this.onClick.bind(this)}>
<Menu onClick={(e) => {
Setting.changeLanguage(e.key);
}}>
<Menu.Item key="en" icon={<IconFont type="icon-en" />}>English</Menu.Item>
<Menu.Item key="zh" icon={<IconFont type="icon-zh" />}>简体中文</Menu.Item>
<Menu.Item key="fr" icon={<IconFont type="icon-fr" />}>Français</Menu.Item>

View File

@@ -21,6 +21,7 @@ import i18next from "i18next";
import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import moment from "moment";
export let ServerUrl = "";
@@ -43,7 +44,7 @@ export function initServerUrl() {
}
}
function isLocalhost() {
export function isLocalhost() {
const hostname = window.location.hostname;
return hostname === "localhost";
}
@@ -308,20 +309,43 @@ export function getAvatarColor(s) {
return colorList[random % 4];
}
export function setLanguage() {
let language = localStorage.getItem('language');
if (language === undefined) {
language = "en"
}
i18next.changeLanguage(language)
export function setLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);
i18next.changeLanguage(language);
}
export function changeLanguage(language) {
localStorage.setItem("language", language)
i18next.changeLanguage(language)
localStorage.setItem("language", language);
changeMomentLanguage(language);
i18next.changeLanguage(language);
window.location.reload(true);
}
export function changeMomentLanguage(language) {
return;
if (language === "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 getClickable(text) {
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid

View File

@@ -236,11 +236,51 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.location} onChange={e => {
this.updateUserField('location', e.target.value);
}} />
</Col>
</Row>
{
(this.state.application === null || this.state.user === null) ? null : (
<AffiliationSelect labelSpan={(Setting.isMobile()) ? 22 : 2} application={this.state.application} user={this.state.user} onUpdateUserField={(key, value) => { return this.updateUserField(key, value)}} />
)
}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.title} onChange={e => {
this.updateUserField('title', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.homepage} onChange={e => {
this.updateUserField('homepage', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.bio} onChange={e => {
this.updateUserField('bio', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :

View File

@@ -0,0 +1,32 @@
// 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 {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({ width = 24, height = 24, color }) {
return <img src={`${StaticBaseUrl}/buttons/gitlab.svg`} alt="Sign in with GitLab" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with GitLab",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "rgb(255,255,255)", color: "#000000"},
activeStyle: {background: "rgb(100,150,250)"},
};
const GitLabLoginButton = createButton(config);
export default GitLabLoginButton;

View File

@@ -32,6 +32,7 @@ import i18next from "i18next";
import LinkedInLoginButton from "./LinkedInLoginButton";
import WeComLoginButton from "./WeComLoginButton";
import LarkLoginButton from "./LarkLoginButton";
import GitLabLoginButton from "./GitLabLoginButton";
class LoginPage extends React.Component {
constructor(props) {
@@ -170,6 +171,8 @@ class LoginPage extends React.Component {
return <WeComLoginButton text={text} align={"center"} />
} else if (type === "Lark") {
return <LarkLoginButton text={text} align={"center"} />
} else if (type === "GitLab") {
return <GitLabLoginButton text={text} align={"center"} />
}
return text;

View File

@@ -43,7 +43,7 @@ const WeiboAuthScope = "email";
const WeiboAuthUri = "https://api.weibo.com/oauth2/authorize";
const WeiboAuthLogo = `${StaticBaseUrl}/img/social_weibo.png`;
const GiteeAuthScope = "user_info,emails";
const GiteeAuthScope = "user_info%20emails";
const GiteeAuthUri = "https://gitee.com/oauth/authorize";
const GiteeAuthLogo = `${StaticBaseUrl}/img/social_gitee.png`;
@@ -51,14 +51,19 @@ const LinkedInAuthScope = "r_liteprofile%20r_emailaddress";
const LinkedInAuthUri = "https://www.linkedin.com/oauth/v2/authorization";
const LinkedInAuthLogo = `${StaticBaseUrl}/img/social_linkedin.png`;
// const WeComAuthScope = "";
const WeComSilentAuthScope = "snsapi_userinfo";
const WeComAuthUri = "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect";
const WeComSilentAuthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize";
const WeComAuthLogo = `${StaticBaseUrl}/img/social_wecom.png`;
// const WeComAuthScope = "";
// const LarkAuthScope = "";
const LarkAuthUri = "https://open.feishu.cn/open-apis/authen/v1/index";
const LarkAuthLogo = `${StaticBaseUrl}/img/social_lark.png`;
const GitLabAuthScope = "read_user+profile";
const GitLabAuthUri = "https://gitlab.com/oauth/authorize";
const GitLabAuthLogo = `${StaticBaseUrl}/img/social_gitlab.png`;
export function getAuthLogo(provider) {
if (provider.type === "Google") {
return GoogleAuthLogo;
@@ -82,6 +87,8 @@ export function getAuthLogo(provider) {
return WeComAuthLogo;
} else if (provider.type === "Lark") {
return LarkAuthLogo;
} else if (provider.type === "GitLab") {
return GitLabAuthLogo;
}
}
@@ -109,10 +116,18 @@ export function getAuthUrl(application, provider, method) {
} else if (provider.type === "Gitee") {
return `${GiteeAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${GiteeAuthScope}&response_type=code&state=${state}`;
} else if (provider.type === "LinkedIn") {
return `${LinkedInAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${LinkedInAuthScope}&response_type=code&state=${state}`
return `${LinkedInAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${LinkedInAuthScope}&response_type=code&state=${state}`;
} else if (provider.type === "WeCom") {
return `${WeComAuthUri}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`
if (provider.method === "Silent") {
return `${WeComSilentAuthUrl}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${WeComSilentAuthScope}&response_type=code#wechat_redirect`;
} else if (provider.method === "Normal") {
return `${WeComAuthUri}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
} else {
return `https://error:not-supported-provider-method:${provider.method}`;
}
} else if (provider.type === "Lark") {
return `${LarkAuthUri}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`
return `${LarkAuthUri}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
} else if (provider.type === "GitLab") {
return `${GitLabAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${GitLabAuthScope}`;
}
}

View File

@@ -62,3 +62,10 @@ export function deleteApplication(application) {
body: JSON.stringify(newApplication),
}).then(res => res.json());
}
export function getApplicationByClientId(clientId){
return fetch(`${Setting.ServerUrl}/api/get-application-clientId?clientId=${clientId}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}

View File

@@ -0,0 +1,47 @@
// 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 * as Setting from "../Setting";
export function PaySuccess(paymentId, payerID){
return fetch(`${Setting.ServerUrl}/api/success-pay?paymentId=${paymentId}&payerId=${payerID}`, {
method: "GET",
credentials: "include",
}).then(res => res.json())
}
export function PaypalPal(payItem,clientId, redirectUri) {
return fetch(`http://localhost:8000/api/paypal?clientId=${clientId}&redirectUri=${redirectUri}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(payItem),
}).then(res => res.json());
}
export function getPayments(){
return fetch(`${Setting.ServerUrl}/api/get-payments?`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function deletePayment(payment){
let newPayment = Setting.deepCopy(payment);
return fetch(`${Setting.ServerUrl}/api/delete-payment`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newPayment),
}).then(res => res.json());
}

View File

@@ -14,8 +14,8 @@
import * as Setting from "../Setting";
export function getResources(owner) {
return fetch(`${Setting.ServerUrl}/api/get-resources?owner=${owner}`, {
export function getResources(owner, user) {
return fetch(`${Setting.ServerUrl}/api/get-resources?owner=${owner}&user=${user}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
@@ -55,10 +55,11 @@ export function deleteResource(resource, provider="") {
}).then(res => res.json());
}
export function uploadResource(owner, tag, parent, fullFilePath, file, provider="") {
export function uploadResource(owner, user, tag, parent, fullFilePath, file, provider="") {
const application = "app-built-in";
let formData = new FormData();
formData.append("file", file);
return fetch(`${Setting.ServerUrl}/api/upload-resource?owner=${owner}&tag=${tag}&parent=${parent}&fullFilePath=${encodeURIComponent(fullFilePath)}&provider=${provider}`, {
return fetch(`${Setting.ServerUrl}/api/upload-resource?owner=${owner}&user=${user}&application=${application}&tag=${tag}&parent=${parent}&fullFilePath=${encodeURIComponent(fullFilePath)}&provider=${provider}`, {
body: formData,
method: 'POST',
credentials: 'include',

View File

@@ -12,38 +12,85 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import i18n from 'i18next'
import zh from './locales/zh/data.json'
import en from './locales/en/data.json'
import fr from './locales/fr/data.json'
import de from './locales/de/data.json'
import ko from './locales/ko/data.json'
import ru from './locales/ru/data.json'
import ja from './locales/ja/data.json'
import i18n from "i18next";
import zh from "./locales/zh/data.json";
import en from "./locales/en/data.json";
import fr from "./locales/fr/data.json";
import de from "./locales/de/data.json";
import ko from "./locales/ko/data.json";
import ru from "./locales/ru/data.json";
import ja from "./locales/ja/data.json";
import * as Conf from "./Conf";
import * as Setting from "./Setting";
const resources = {
en: en,
zh: zh,
fr: fr,
ja: ja,
de: de,
ko: ko,
ru: ru,
ja: ja,
};
i18n
.init({
lng: "en",
function initLanguage() {
let language = localStorage.getItem("language");
if (language === undefined || language == null) {
if (Conf.ForceLanguage !== "") {
language = Conf.ForceLanguage;
} else {
let userLanguage;
userLanguage = navigator.language;
switch (userLanguage) {
case "zh-CN":
language = "zh";
break;
case "zh":
language = "zh";
break;
case "en":
language = "en";
break;
case "en-US":
language = "en";
break;
case "fr":
language = "fr";
break;
case "de":
language = "de";
break;
case "ko":
language = "ko";
break;
case "ru":
language = "ru";
break;
case "ja":
language = "ja";
break;
default:
language = Conf.DefaultLanguage;
}
}
}
Setting.changeMomentLanguage(language);
resources: resources,
return language;
}
keySeparator: false,
i18n.init({
lng: initLanguage(),
interpolation: {
escapeValue: false
},
saveMissing: true,
})
resources: resources,
export default i18n;
keySeparator: false,
interpolation: {
escapeValue: false,
},
//debug: true,
saveMissing: true,
});
export default i18n;

View File

@@ -79,7 +79,12 @@
"Signin URL": "Signin URL",
"Signin URL - Tooltip": "sign in url",
"ID - Tooltip": "random string",
"Favicon - Tooltip": "Application icon"
"Favicon - Tooltip": "Application icon",
"Uploading": "Uploading",
"Start Upload": "Start Upload",
"Upload success": "Upload success",
"Back Home": "Back Home",
"Sorry, the page you visited does not exist": "Sorry, the page you visited does not exist"
},
"signup": {
"Username": "Username",
@@ -159,6 +164,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
@@ -186,7 +193,13 @@
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Region - Tooltip": "Storage region",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "Edit User",

View File

@@ -171,6 +171,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",

View File

@@ -79,7 +79,12 @@
"Signin URL": "Signin URL",
"Signin URL - Tooltip": "sign in url",
"ID - Tooltip": "random string",
"Favicon - Tooltip": "Application icon"
"Favicon - Tooltip": "Application icon",
"Uploading": "Uploading",
"Start Upload": "Start Upload",
"Upload success": "Upload success",
"Back Home": "Back Home",
"Sorry, the page you visited does not exist": "Sorry, the page you visited does not exist"
},
"signup": {
"Username": "Username",
@@ -159,6 +164,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
@@ -186,7 +193,13 @@
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Region - Tooltip": "Storage region",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "Edit User",

View File

@@ -79,7 +79,12 @@
"Signin URL": "Signin URL",
"Signin URL - Tooltip": "sign in url",
"ID - Tooltip": "random string",
"Favicon - Tooltip": "Application icon"
"Favicon - Tooltip": "Application icon",
"Uploading": "Uploading",
"Start Upload": "Start Upload",
"Upload success": "Upload success",
"Back Home": "Back Home",
"Sorry, the page you visited does not exist": "Sorry, the page you visited does not exist"
},
"signup": {
"Username": "Username",
@@ -159,6 +164,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
@@ -186,7 +193,13 @@
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Region - Tooltip": "Storage region",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "Edit User",

View File

@@ -79,7 +79,12 @@
"Signin URL": "Signin URL",
"Signin URL - Tooltip": "sign in url",
"ID - Tooltip": "random string",
"Favicon - Tooltip": "Application icon"
"Favicon - Tooltip": "Application icon",
"Uploading": "Uploading",
"Start Upload": "Start Upload",
"Upload success": "Upload success",
"Back Home": "Back Home",
"Sorry, the page you visited does not exist": "Sorry, the page you visited does not exist"
},
"signup": {
"Username": "Username",
@@ -159,6 +164,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
@@ -186,7 +193,13 @@
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Region - Tooltip": "Storage region",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "Edit User",

View File

@@ -79,7 +79,12 @@
"Signin URL": "Signin URL",
"Signin URL - Tooltip": "sign in url",
"ID - Tooltip": "random string",
"Favicon - Tooltip": "Application icon"
"Favicon - Tooltip": "Application icon",
"Uploading": "Uploading",
"Start Upload": "Start Upload",
"Upload success": "Upload success",
"Back Home": "Back Home",
"Sorry, the page you visited does not exist": "Sorry, the page you visited does not exist"
},
"signup": {
"Username": "Username",
@@ -159,6 +164,8 @@
"Category - Tooltip": "Unique string-style identifier",
"Type": "Type",
"Type - Tooltip": "Unique string-style identifier",
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Client ID": "Client ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
@@ -186,7 +193,13 @@
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Region - Tooltip": "Storage region",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "Edit User",

View File

@@ -12,11 +12,11 @@
"User": "用户",
"Applications": "应用",
"Application": "应用",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Records": "日志",
"Client ip": "客户端地址",
"Timestamp": "时间戳",
"Username": "用户名",
"Request uri": "Request uri",
"Request uri": "请求地址",
"LDAPs": "LDAPs",
"Save": "保存",
"Add": "添加",
@@ -79,7 +79,12 @@
"Signin URL": "登录URL",
"Signin URL - Tooltip": "用户的登录地址",
"ID - Tooltip": "唯一的随机字符串",
"Favicon - Tooltip": "网站的图标"
"Favicon - Tooltip": "网站的图标",
"Uploading": "上传中",
"Start Upload": "开始上传",
"Upload success": "上传成功",
"Back Home": "返回到首页",
"Sorry, the page you visited does not exist": "很抱歉,你访问的页面不存在"
},
"signup": {
"Username": "用户名",
@@ -87,7 +92,7 @@
"Please input your personal name!": "请输入您的姓名!",
"Please input your address!": "请输入您的地址!",
"Please input your affiliation!": "请输入您所在的工作单位!",
"Please select your country/region!": "Please select your country/region!",
"Please select your country/region!": "请选择您的国家/地区",
"The input is not valid Email!": "您输入的电子邮箱格式错误!",
"Please input your Email!": "请输入您的电子邮箱!",
"Confirm": "确认密码",
@@ -159,6 +164,8 @@
"Category - Tooltip": "唯一的、字符串式的ID",
"Type": "类型",
"Type - Tooltip": "唯一的、字符串式的ID",
"Method": "方式",
"Method - Tooltip": "登录行为,二维码或者静默授权登录",
"Client ID": "Client ID",
"Client ID - Tooltip": "唯一的、字符串式的ID",
"Client secret": "Client secret",
@@ -179,14 +186,20 @@
"Provider URL": "提供商URL",
"Provider URL - Tooltip": "唯一的、字符串式的ID",
"Edit Provider": "修改提供商",
"Endpoint": "Endpoint",
"Endpoint": "节点",
"Endpoint - Tooltip": "Storage bucket endpoint",
"Bucket": "Bucket",
"Bucket - Tooltip": "Storage bucket name",
"Domain": "Domain",
"Domain - Tooltip": "Storage endpoint custom domain",
"Region": "Region",
"Region - Tooltip": "Storage region"
"Bucket": "存储桶",
"Bucket - Tooltip": "存储桶名称",
"Domain": "域名",
"Domain - Tooltip": "存储节点自定义域名",
"Region": "地区",
"Region - Tooltip": "存储区域",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key - Tooltip",
"SMS account": "SMS账号",
"SMS account - Tooltip": "SMS account - Tooltip"
},
"user": {
"Edit User": "修改用户",
@@ -199,8 +212,8 @@
"Address - Tooltip": "唯一的、字符串式的ID",
"Affiliation": "工作单位",
"Affiliation - Tooltip": "唯一的、字符串式的ID",
"Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region",
"Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区",
"Modify affiliation": "修改工作单位",
"Tag": "标签",
"Tag - Tooltip": "唯一的、字符串式的ID",