Compare commits

...

40 Commits

Author SHA1 Message Date
eeda5e5629 Improve footer. 2021-07-18 17:50:50 +08:00
bce606896f Use signup app for GetApplicationByUser(). 2021-07-18 17:50:38 +08:00
4ff4961312 Merge pull request #153 from Kininaru/auto-login
feat: auto login session will expire after 24h
2021-07-18 11:30:00 +08:00
b2dd8f4fa3 feat: session without autosignin will expire
Signed-off-by: Kininaru <shiftregister233@outlook.com>
2021-07-18 07:54:49 +08:00
ae9ebd2de1 refactor: SessionUser -> SessionUsername
Signed-off-by: Kininaru <shiftregister233@outlook.com>
2021-07-18 07:15:22 +08:00
441d69f4ac Merge pull request #177 from oranges-eating/master
fix: Fix the user list cannot be displayed completely
2021-07-17 22:55:00 +08:00
1550956c8e fix: Fix the user list cannot be displayed completely
Signed-off-by: killer <1533063601@qq.com>
2021-07-17 14:29:33 +08:00
0967217778 Sync from crowdin 2021-07-17 06:14:09 +00:00
3820a0185c feat: support LDAP (#160)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2021-07-17 14:13:00 +08:00
3905df8546 Sync from crowdin 2021-07-17 01:33:19 +00:00
e16ff7f4a9 Merge pull request #174 from WindSpiritSR/patch-fix-err
fix: db data init and frontend warning
2021-07-17 09:32:09 +08:00
bbec117fd6 fix: db data init and frontend warning
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2021-07-16 23:04:39 +08:00
8ae4e30620 Sync from crowdin 2021-07-16 14:38:41 +00:00
814ab9c11f Merge pull request #175 from turbodog03/master
feat: add language select box and background color change when hover
2021-07-16 22:37:31 +08:00
78c5757d85 feat: add language select box and background color change when hover
Signed-off-by: turbodog03 <63595854+turbodog03@users.noreply.github.com>
2021-07-16 22:32:29 +08:00
ee92a9b7b4 Merge pull request #162 from oranges-eating/master
feat: add run casdoor through docker
2021-07-14 17:24:33 +08:00
21eb1e8037 feat: add run casdoor through docker
Signed-off-by: killer <1533063601@qq.com>
2021-07-14 14:44:02 +08:00
2297251dd7 Sync from crowdin 2021-07-13 15:06:48 +00:00
532dc75033 Merge pull request #164 from MRGUOKING/door-dev1
fix: The count-down will be disabled
2021-07-13 23:05:25 +08:00
e7de0e4132 fix: The count-down will be disabled
Signed-off-by: MRGUOKING <420919469@qq.com>

The count-down will be disabled after sending the code

Signed-off-by: MRGUOKING <420919469@qq.com>
2021-07-13 21:02:14 +08:00
7e6af1e858 Fix adding provider UI bug. 2021-07-12 01:08:48 +08:00
cca6a635c3 Add defaultAvatar column. 2021-07-12 00:00:59 +08:00
a355798a79 Replace getDefaultApplication() with getUserApplication(). 2021-07-11 23:51:01 +08:00
14445e7c3b Sync from crowdin 2021-07-10 10:05:24 +00:00
aae09648e9 Merge pull request #150 from oranges-eating/master
feat: Add log table and record all user behaviors into the table, add UI to view logs
2021-07-10 18:04:11 +08:00
a95b168a54 feat: add UI to view logs
Signed-off-by: killer <1533063601@qq.com>
2021-07-10 17:27:21 +08:00
834693a0a2 Sync from crowdin 2021-07-10 02:32:12 +00:00
7b1386764c Improve translation. 2021-07-10 10:31:01 +08:00
b7077c61be Sync from crowdin 2021-07-09 18:19:56 +00:00
4c27ae68fb Merge pull request #159 from ErikQQY/master
fix: Change commit author to casbin bot
2021-07-10 02:18:47 +08:00
1576c01d8f fix: Change commit author to casbin bot
Signed-off-by: ErikQQY <2283984853@qq.com>
2021-07-10 02:09:26 +08:00
968eccf193 Restrict app edit page values. 2021-07-10 01:19:31 +08:00
8eb5a7b163 Restrict rule options. 2021-07-10 00:08:43 +08:00
4a6ec33b9c Refactor out Setting.getDeduplicatedArray() 2021-07-10 00:05:40 +08:00
f011dc06d8 Refactor out Setting.getArrayItem() 2021-07-09 23:05:50 +08:00
d409de6591 Merge pull request #156 from ErikQQY/master
fix: Update yarn.lock
2021-07-09 22:39:49 +08:00
c9b6bc79a2 fix: Update yarn.lock
Signed-off-by: ErikQQY <2283984853@qq.com>
2021-07-09 22:07:51 +08:00
dd4f197454 Merge pull request #144 from sh1luo/feat-add-linkedin-provider
feat: add linkedin provider
2021-07-09 17:07:29 +08:00
576d0f12dd Sync from crowdin 2021-07-09 08:17:04 +00:00
c37e5d044a feat: add linkedin provider
Signed-off-by: wasabi <690898835@qq.com>
2021-07-04 16:28:52 +08:00
71 changed files with 3850 additions and 240 deletions

View File

@ -35,3 +35,4 @@ jobs:
with:
commit_message: Sync from crowdin
file_pattern: web/src/locales/*
commit_author: ${{ secrets.CROWDIN_BOT }}

View File

@ -47,6 +47,18 @@ Backend | RESTful API backend for Casdoor | Golang + Beego + MySQL | https://git
git clone https://github.com/casbin/casdoor
```
## Run through Docker
- Install Docker and Docker-compose,you see [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/)
- vi casdoor/conf/app.conf
- Modify dataSourceName = root:123@tcp(localhost:3306)/ to dataSourceName = root:123@tcp(db:3306)/
- Execute the following command
```shell
docker-compose up
```
- Open browser:
http://localhost:8000/
## Run (Dev Environment)
- Run backend (in port 8000):

View File

@ -82,7 +82,7 @@ p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-users, *, *
p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-organizations, *, *
p, *, *, GET, /api/get-default-application, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-default-providers, *, *
p, *, *, POST, /api/upload-avatar, *, *
p, *, *, POST, /api/unlink, *, *

View File

@ -52,6 +52,8 @@ type RequestForm struct {
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
PhonePrefix string `json:"phonePrefix"`
AutoSignin bool `json:"autoSignin"`
}
type Response struct {
@ -78,8 +80,8 @@ type HumanCheck struct {
func (c *ApiController) Signup() {
var resp Response
if c.GetSessionUser() != "" {
c.ResponseErrorWithData("Please sign out first before signing up", c.GetSessionUser())
if c.GetSessionUsername() != "" {
c.ResponseErrorWithData("Please sign out first before signing up", c.GetSessionUsername())
return
}
@ -161,7 +163,7 @@ func (c *ApiController) Signup() {
if application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUser(user.GetId())
c.SetSessionUsername(user.GetId())
}
object.DisableVerificationCode(form.Email)
@ -181,10 +183,11 @@ func (c *ApiController) Signup() {
func (c *ApiController) Logout() {
var resp Response
user := c.GetSessionUser()
user := c.GetSessionUsername()
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
c.SetSessionUser("")
c.SetSessionUsername("")
c.SetSessionData(nil)
resp = Response{Status: "ok", Msg: "", Data: user}

View File

@ -44,23 +44,19 @@ func (c *ApiController) GetApplication() {
c.ServeJSON()
}
// @Title GetDefaultApplication
// @Description get the detail of the default application
// @Param owner query string true "The owner of the application."
// @Title GetUserApplication
// @Description get the detail of the user's application
// @Param id query string true "The id of the user"
// @Success 200 {object} object.Application The Response object
// @router /get-default-application [get]
func (c *ApiController) GetDefaultApplication() {
//owner := c.Input().Get("owner")
if c.GetSessionUser() == "" {
c.Data["json"] = nil
c.ServeJSON()
// @router /get-user-application [get]
func (c *ApiController) GetUserApplication() {
id := c.Input().Get("id")
user := object.GetUser(id)
if user == nil {
c.ResponseError("No such user.")
return
}
username := c.GetSessionUser()
user := object.GetUser(username)
c.Data["json"] = object.GetApplicationByUser(user)
c.ServeJSON()
}

View File

@ -19,6 +19,7 @@ import (
"fmt"
"strconv"
"strings"
"time"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/idp"
@ -38,7 +39,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
userId := user.GetId()
resp := &Response{}
if form.Type == ResponseTypeLogin {
c.SetSessionUser(userId)
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
resp = &Response{Status: "ok", Msg: "", Data: userId}
} else if form.Type == ResponseTypeCode {
@ -53,11 +54,21 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUser(userId)
c.SetSessionUsername(userId)
}
} else {
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
}
// if user did not check auto signin
if resp.Status == "ok" && !form.AutoSignin {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
}
return resp
}
@ -108,8 +119,8 @@ func (c *ApiController) Login() {
if form.Username != "" {
if form.Type == ResponseTypeLogin {
if c.GetSessionUser() != "" {
resp = &Response{Status: "error", Msg: "Please log out first before signing in", Data: c.GetSessionUser()}
if c.GetSessionUsername() != "" {
resp = &Response{Status: "error", Msg: "Please log out first before signing in", Data: c.GetSessionUsername()}
c.Data["json"] = resp
c.ServeJSON()
return
@ -181,6 +192,12 @@ func (c *ApiController) Login() {
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
resp = c.HandleLoggedIn(application, user, &form)
record := util.Records(c.Ctx)
record.Organization = application.Organization
record.Username = user.Name
object.AddRecord(record)
}
} else if form.Provider != "" {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
@ -252,6 +269,12 @@ func (c *ApiController) Login() {
//}
resp = c.HandleLoggedIn(application, user, &form)
record := util.Records(c.Ctx)
record.Organization = application.Organization
record.Username = user.Name
object.AddRecord(record)
} else {
// Sign up via OAuth
if !application.EnableSignUp {
@ -294,10 +317,16 @@ func (c *ApiController) Login() {
object.LinkUserAccount(user, provider.Type, userInfo.Id)
resp = c.HandleLoggedIn(application, user, &form)
record := util.Records(c.Ctx)
record.Organization = application.Organization
record.Username = user.Name
object.AddRecord(record)
}
//resp = &Response{Status: "ok", Msg: "", Data: res}
} else { // form.Method != "signup"
userId := c.GetSessionUser()
userId := c.GetSessionUsername()
if userId == "" {
resp = &Response{Status: "error", Msg: "The account does not exist", Data: userInfo}
c.Data["json"] = resp

View File

@ -14,13 +14,32 @@
package controllers
import "github.com/astaxie/beego"
import (
"time"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/util"
)
type ApiController struct {
beego.Controller
}
func (c *ApiController) GetSessionUser() string {
type SessionData struct {
ExpireTime int64
}
func (c *ApiController) GetSessionUsername() string {
// check if user session expired
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
return ""
}
user := c.GetSession("username")
if user == nil {
return ""
@ -29,10 +48,34 @@ func (c *ApiController) GetSessionUser() string {
return user.(string)
}
func (c *ApiController) SetSessionUser(user string) {
func (c *ApiController) SetSessionUsername(user string) {
c.SetSession("username", user)
}
func (c *ApiController) GetSessionData() *SessionData {
session := c.GetSession("SessionData")
if session == nil {
return nil
}
sessionData := &SessionData{}
err := util.JsonToStruct(session.(string), sessionData)
if err != nil {
panic(err)
}
return sessionData
}
func (c *ApiController) SetSessionData(s *SessionData) {
if s == nil {
c.DelSession("SessionData")
return
}
c.SetSession("SessionData", util.StructToJson(s))
}
func wrapActionResponse(affected bool) *Response {
if affected {
return &Response{Status: "ok", Msg: "", Data: "Affected"}

209
controllers/ldap.go Normal file
View File

@ -0,0 +1,209 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type LdapServer struct {
Host string `json:"host"`
Port int `json:"port"`
Admin string `json:"admin"`
Passwd string `json:"passwd"`
BaseDn string `json:"baseDn"`
}
type LdapResp struct {
//Groups []LdapRespGroup `json:"groups"`
Users []object.LdapRespUser `json:"users"`
}
//type LdapRespGroup struct {
// GroupId string
// GroupName string
//}
type LdapSyncResp struct {
Exist []object.LdapRespUser `json:"exist"`
Failed []object.LdapRespUser `json:"failed"`
}
func (c *ApiController) GetLdapUser() {
ldapServer := LdapServer{}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldapServer)
if err != nil || util.IsStrsEmpty(ldapServer.Host, ldapServer.Admin, ldapServer.Passwd, ldapServer.BaseDn) {
c.ResponseError("Missing parameter")
return
}
var resp LdapResp
conn, err := object.GetLdapConn(ldapServer.Host, ldapServer.Port, ldapServer.Admin, ldapServer.Passwd)
if err != nil {
c.Data["json"] = Response{Status: "error", Msg: err.Error()}
c.ServeJSON()
return
}
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
//if err != nil {
// c.Data["json"] = Response{Status: "error", Msg: err.Error()}
// c.ServeJSON()
// return
//}
//for _, group := range groupsMap {
// resp.Groups = append(resp.Groups, LdapRespGroup{
// GroupId: group.GidNumber,
// GroupName: group.Cn,
// })
//}
users, err := conn.GetLdapUsers(ldapServer.BaseDn)
if err != nil {
c.Data["json"] = Response{Status: "error", Msg: err.Error()}
c.ServeJSON()
return
}
for _, user := range users {
resp.Users = append(resp.Users, object.LdapRespUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
//GroupName: groupsMap[user.GidNumber].Cn,
Uuid: user.Uuid,
Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress),
Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber),
Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress),
})
}
c.Data["json"] = Response{Status: "ok", Data: resp}
c.ServeJSON()
return
}
func (c *ApiController) GetLdaps() {
owner := c.Input().Get("owner")
c.Data["json"] = Response{Status: "ok", Data: object.GetLdaps(owner)}
c.ServeJSON()
}
func (c *ApiController) GetLdap() {
id := c.Input().Get("id")
if util.IsStrsEmpty(id) {
c.ResponseError("Missing parameter")
return
}
c.Data["json"] = Response{Status: "ok", Data: object.GetLdap(id)}
c.ServeJSON()
}
func (c *ApiController) AddLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil {
c.ResponseError("Missing parameter")
return
}
if util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) {
c.ResponseError("Missing parameter")
return
}
if object.CheckLdapExist(&ldap) {
c.ResponseError("Ldap server exist")
return
}
affected := object.AddLdap(&ldap)
resp := wrapActionResponse(affected)
if affected {
resp.Data2 = ldap
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) UpdateLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil || util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) {
c.ResponseError("Missing parameter")
return
}
affected := object.UpdateLdap(&ldap)
resp := wrapActionResponse(affected)
if affected {
resp.Data2 = ldap
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) DeleteLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteLdap(&ldap))
c.ServeJSON()
}
func (c *ApiController) SyncLdapUsers() {
owner := c.Input().Get("owner")
ldapId := c.Input().Get("ldapId")
var users []object.LdapRespUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil {
panic(err)
}
object.UpdateLdapSyncTime(ldapId)
exist, failed := object.SyncLdapUsers(owner, users)
c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{
Exist: *exist,
Failed: *failed,
}}
c.ServeJSON()
}
func (c *ApiController) CheckLdapUsersExist() {
owner := c.Input().Get("owner")
var uuids []string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &uuids)
if err != nil {
panic(err)
}
exist := object.CheckLdapUuidExist(owner, uuids)
c.Data["json"] = &Response{Status: "ok", Data: exist}
c.ServeJSON()
}

46
controllers/record.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
)
// @Title GetRecords
// @Description get all records
// @Success 200 {array} object.Records The Response object
// @router /get-records [get]
func (c *ApiController) GetRecords() {
c.Data["json"] = object.GetRecords()
c.ServeJSON()
}
// @Title GetRecordsByFilter
// @Description get records by filter
// @Param body body object.Records true "filter Record message"
// @Success 200 {array} object.Records The Response object
// @router /get-records-filter [post]
func (c *ApiController) GetRecordsByFilter() {
var record object.Records
err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
if err != nil {
panic(err)
}
c.Data["json"] = object.GetRecordsByField(&record)
c.ServeJSON()
}

View File

@ -170,7 +170,7 @@ func (c *ApiController) SetPassword() {
oldPassword := c.Ctx.Request.Form.Get("oldPassword")
newPassword := c.Ctx.Request.Form.Get("newPassword")
requestUserId := c.GetSessionUser()
requestUserId := c.GetSessionUsername()
if requestUserId == "" {
c.ResponseError("Please login first.")
return
@ -223,7 +223,7 @@ func (c *ApiController) SetPassword() {
return
}
c.SetSessionUser("")
c.SetSessionUsername("")
targetUser.Password = newPassword
object.SetUserField(targetUser, "password", targetUser.Password)

View File

@ -60,7 +60,7 @@ func (c *ApiController) ResponseErrorWithData(error string, data interface{}) {
}
func (c *ApiController) RequireSignedIn() (string, bool) {
userId := c.GetSessionUser()
userId := c.GetSessionUsername()
if userId == "" {
resp := Response{Status: "error", Msg: "Please sign in first"}
c.Data["json"] = resp

View File

@ -24,7 +24,7 @@ import (
func (c *ApiController) getCurrentUser() *object.User {
var user *object.User
userId := c.GetSessionUser()
userId := c.GetSessionUsername()
if userId == "" {
user = nil
} else {

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: '3.1'
services:
casdoor:
build:
context: ./
dockerfile: go-dockerfile
ports:
- 8000:8000
depends_on:
- db
db:
restart: always
image: mysql:8.0.25
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- /usr/local/docker/mysql:/var/lib/mysql

25
go-dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM golang:1.17-rc-buster
WORKDIR /casdoor
COPY ./ /casdoor
RUN go env -w CGO_ENABLED=0 GOPROXY=https://goproxy.io,direct GOOS=linux GOARCH=amd64 \
&& apt update && apt install sudo \
&& wget https://nodejs.org/dist/v12.22.0/node-v12.22.0-linux-x64.tar.gz \
&& sudo tar xf node-v12.22.0-linux-x64.tar.gz \
&& sudo apt install wait-for-it
ENV PATH=$PATH:/casdoor/node-v12.22.0-linux-x64/bin
RUN npm install -g yarn \
&& cd web \
&& yarn install \
&& yarn run build \
&& rm -rf node_modules \
&& cd /casdoor \
&& go build main.go
FROM alpine:3.7
COPY --from=0 /casdoor /
COPY --from=0 /usr/bin/wait-for-it /
RUN set -eux \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
&& apk update \
&& apk upgrade \
&& apk add bash
CMD ./wait-for-it db:3306 && ./main

1
go.mod
View File

@ -13,6 +13,7 @@ require (
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
github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-sql-driver/mysql v1.5.0
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect

7
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -63,10 +65,14 @@ github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ=
github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
@ -220,6 +226,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

331
idp/linkedin.go Normal file
View File

@ -0,0 +1,331 @@
// 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"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2"
)
type LinkedInIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewLinkedInIdProvider(clientId string, clientSecret string, redirectUrl string) *LinkedInIdProvider {
idp := &LinkedInIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
func (idp *LinkedInIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *LinkedInIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
TokenURL: "https://www.linkedIn.com/oauth/v2/accessToken",
}
var config = &oauth2.Config{
Scopes: []string{"email,public_profile"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type LinkedInAccessToken struct {
AccessToken string `json:"access_token"` //Interface call credentials
ExpiresIn int64 `json:"expires_in"` //access_token interface call credential timeout time, unit (seconds)
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://docs.microsoft.com/en-us/linkedIn/shared/authentication/authorization-code-flow?context=linkedIn%2Fcontext&tabs=HTTPS
func (idp *LinkedInIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := url.Values{}
params.Add("grant_type", "authorization_code")
params.Add("redirect_uri", idp.Config.RedirectURL)
params.Add("client_id", idp.Config.ClientID)
params.Add("client_secret", idp.Config.ClientSecret)
params.Add("code", code)
accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode())
bs, _ := json.Marshal(params.Encode())
req, _ := http.NewRequest("POST", accessTokenUrl, strings.NewReader(string(bs)))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
rbs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
tokenResp := LinkedInAccessToken{}
if err = json.Unmarshal(rbs, &tokenResp); err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: tokenResp.AccessToken,
TokenType: "Bearer",
Expiry: time.Unix(time.Now().Unix()+tokenResp.ExpiresIn, 0),
}
return token, nil
}
/*
{
"firstName": {
"localized": {
"zh_CN": "继坤"
},
"preferredLocale": {
"country": "CN",
"language": "zh"
}
},
"lastName": {
"localized": {
"zh_CN": "刘"
},
"preferredLocale": {
"country": "CN",
"language": "zh"
}
},
"profilePicture": {
"displayImage": "urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg",
"displayImage~": {
"paging": {
"count": 10,
"start": 0,
"links": []
},
"elements": [
{
"artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100)",
"authorizationMethod": "PUBLIC",
"data": {
"com.linkedin.digitalmedia.mediaartifact.StillImage": {
"mediaType": "image/jpeg",
"rawCodecSpec": {
"name": "jpeg",
"type": "image"
},
"displaySize": {
"width": 100.0,
"uom": "PX",
"height": 100.0
},
"storageSize": {
"width": 100,
"height": 100
},
"storageAspectRatio": {
"widthAspect": 1.0,
"heightAspect": 1.0,
"formatted": "1.00:1.00"
},
"displayAspectRatio": {
"widthAspect": 1.0,
"heightAspect": 1.0,
"formatted": "1.00:1.00"
}
}
},
"identifiers": [
{
"identifier": "https://media.licdn.cn/dms/image/C5603AQHbdR8RkG62yg/profile-displayphoto-shrink_100_100/0/1625279434135?e=1630540800&v=beta&t=Z-bQKf_jFv8L1uwr6X5AJLoTQRWZrueT7qrITDSvxWM",
"index": 0,
"mediaType": "image/jpeg",
"file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100,0)",
"identifierType": "EXTERNAL_URL",
"identifierExpiresInSeconds": 1630540800
}
]
},
// ...
}
]
}
},
"id": "vvMfLsLIRs"
}
*/
type LinkedInUserInfo struct {
FirstName struct {
Localized map[string]string `json:"localized"`
PreferredLocale struct {
Country string `json:"country"`
Language string `json:"language"`
} `json:"preferredLocale"`
} `json:"firstName"`
LastName struct {
Localized map[string]string `json:"localized"`
PreferredLocale struct {
Country string `json:"country"`
Language string `json:"language"`
} `json:"preferredLocale"`
} `json:"lastName"`
ProfilePicture struct {
DisplayImage string `json:"displayImage"`
DisplayImage1 struct {
Paging struct {
Count int `json:"count"`
Start int `json:"start"`
Links []interface{} `json:"links"`
} `json:"paging"`
Elements []struct {
Artifact string `json:"artifact"`
AuthorizationMethod string `json:"authorizationMethod"`
Data struct {
ComLinkedinDigitalmediaMediaartifactStillImage struct {
MediaType string `json:"mediaType"`
RawCodecSpec struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"rawCodecSpec"`
DisplaySize struct {
Width float64 `json:"width"`
Uom string `json:"uom"`
Height float64 `json:"height"`
} `json:"displaySize"`
StorageSize struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"storageSize"`
StorageAspectRatio struct {
WidthAspect float64 `json:"widthAspect"`
HeightAspect float64 `json:"heightAspect"`
Formatted string `json:"formatted"`
} `json:"storageAspectRatio"`
DisplayAspectRatio struct {
WidthAspect float64 `json:"widthAspect"`
HeightAspect float64 `json:"heightAspect"`
Formatted string `json:"formatted"`
} `json:"displayAspectRatio"`
} `json:"com.linkedin.digitalmedia.mediaartifact.StillImage"`
} `json:"data"`
Identifiers []struct {
Identifier string `json:"identifier"`
Index int `json:"index"`
MediaType string `json:"mediaType"`
File string `json:"file"`
IdentifierType string `json:"identifierType"`
IdentifierExpiresInSeconds int `json:"identifierExpiresInSeconds"`
} `json:"identifiers"`
} `json:"elements"`
} `json:"displayImage~"`
} `json:"profilePicture"`
Id string `json:"id"`
}
/*
{
"handle": "urn:li:emailAddress:3775708763",
"handle~": {
"emailAddress": "hsimpson@linkedin.com"
}
}
*/
type LinkedInUserEmail struct {
Elements []struct {
Handle struct {
EmailAddress string `json:"emailAddress"`
} `json:"handle~"`
Handle1 string `json:"handle"`
} `json:"elements"`
}
// GetUserInfo use LinkedInAccessToken gotten before return LinkedInUserInfo
// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
func (idp *LinkedInIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
var linkedInUserInfo LinkedInUserInfo
bs, err := idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))", token.AccessToken)
if err != nil {
return nil, err
}
if err = json.Unmarshal(bs, &linkedInUserInfo); err != nil {
return nil, err
}
var linkedInUserEmail LinkedInUserEmail
bs, err = idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/emailAddress?q=members&projection=(elements*(handle~))", token.AccessToken)
if err != nil {
return nil, err
}
if err = json.Unmarshal(bs, &linkedInUserEmail); err != nil {
return nil, err
}
username := ""
for _, name := range linkedInUserInfo.FirstName.Localized {
username += name
}
for _, name := range linkedInUserInfo.LastName.Localized {
username += name
}
userInfo := UserInfo{
Id: linkedInUserInfo.Id,
DisplayName: username,
Username: username,
Email: linkedInUserEmail.Elements[0].Handle.EmailAddress,
AvatarUrl: linkedInUserInfo.ProfilePicture.DisplayImage1.Elements[0].Identifiers[0].Identifier,
}
return &userInfo, nil
}
func (idp *LinkedInIdProvider) GetUrlRespWithAuthorization(url, token string) ([]byte, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return bs, nil
}

View File

@ -51,6 +51,8 @@ func GetIdProvider(providerType string, clientId string, clientSecret string, re
return NewWeiBoIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "Gitee" {
return NewGiteeIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "LinkedIn" {
return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl)
}
return nil

View File

@ -48,6 +48,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoLoginFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"
beego.BConfig.WebConfig.Session.SessionProvider = "file"

View File

@ -133,4 +133,13 @@ func (a *Adapter) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Records))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap))
if err != nil {
panic(err)
}
}

View File

@ -119,7 +119,11 @@ func GetApplicationByOrganizationName(organization string) *Application {
}
func GetApplicationByUser(user *User) *Application {
return GetApplicationByOrganizationName(user.Owner)
if user.SignupApplication != "" {
return getApplication("admin", user.SignupApplication)
} else {
return GetApplicationByOrganizationName(user.Owner)
}
}
func GetApplicationByClientId(clientId string) *Application {

View File

@ -1,3 +1,17 @@
// 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 (

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/casdoor/casdoor/util"
@ -6,6 +20,7 @@ func InitDb() {
initBuiltInOrganization()
initBuiltInUser()
initBuiltInApplication()
initBuiltInLdap()
}
func initBuiltInOrganization() {
@ -15,12 +30,15 @@ func initBuiltInOrganization() {
}
organization = &Organization{
Owner: "admin",
Name: "built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com",
PasswordType: "plain",
Owner: "admin",
Name: "built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com",
Favicon: "https://cdn.casbin.com/static/favicon.ico",
PhonePrefix: "86",
DefaultAvatar: "https://casbin.org/img/casbin.svg",
PasswordType: "plain",
}
AddOrganization(organization)
}
@ -36,11 +54,13 @@ func initBuiltInUser() {
Name: "admin",
CreatedTime: util.GetCurrentTime(),
Id: util.GenerateId(),
Type: "normal-user",
Password: "123",
DisplayName: "Admin",
Avatar: "https://casbin.org/img/casbin.svg",
Email: "admin@example.com",
Phone: "1-12345678",
Phone: "12345678910",
Address: []string{},
Affiliation: "Example Inc.",
Tag: "staff",
IsAdmin: true,
@ -74,3 +94,24 @@ func initBuiltInApplication() {
}
AddApplication(application)
}
func initBuiltInLdap() {
ldap := GetLdap("ldap-built-in")
if ldap != nil {
return
}
ldap = &Ldap{
Id: "ldap-built-in",
Owner: "built-in",
ServerName: "BuildIn LDAP Server",
Host: "example.com",
Port: 389,
Admin: "cn=buildin,dc=example,dc=com",
Passwd: "123",
BaseDn: "ou=BuildIn,dc=example,dc=com",
AutoSync: 0,
LastSync: "",
}
AddLdap(ldap)
}

381
object/ldap.go Normal file
View File

@ -0,0 +1,381 @@
// 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 (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
"strings"
)
type Ldap struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
Admin string `xorm:"varchar(100)" json:"admin"`
Passwd string `xorm:"varchar(100)" json:"passwd"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
}
type ldapConn struct {
Conn *goldap.Conn
}
//type ldapGroup struct {
// GidNumber string
// Cn string
//}
type ldapUser struct {
UidNumber string
Uid string
Cn string
GidNumber string
//Gcn string
Uuid string
Mail string
Email string
EmailAddress string
TelephoneNumber string
Mobile string
MobileTelephoneNumber string
RegisteredAddress string
PostalAddress string
}
type LdapRespUser struct {
UidNumber string `json:"uidNumber"`
Uid string `json:"uid"`
Cn string `json:"cn"`
GroupId string `json:"groupId"`
//GroupName string `json:"groupName"`
Uuid string `json:"uuid"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"address"`
}
func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*ldapConn, error) {
conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, err
}
err = conn.Bind(adminUser, adminPasswd)
if err != nil {
return nil, fmt.Errorf("fail to login Ldap server with [%s]", adminUser)
}
return &ldapConn{Conn: conn}, nil
}
//FIXME: The Base DN does not necessarily contain the Group
//func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
//}
func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
SearchFilter := "(objectClass=posixAccount)"
SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
searchReq := goldap.NewSearchRequest(baseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
searchResult, err := l.Conn.Search(searchReq)
if err != nil {
return nil, err
}
if len(searchResult.Entries) == 0 {
return nil, errors.New("no result")
}
var ldapUsers []ldapUser
for _, entry := range searchResult.Entries {
var ldapUserItem ldapUser
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "uidNumber":
ldapUserItem.UidNumber = attribute.Values[0]
break
case "uid":
ldapUserItem.Uid = attribute.Values[0]
break
case "cn":
ldapUserItem.Cn = attribute.Values[0]
break
case "gidNumber":
ldapUserItem.GidNumber = attribute.Values[0]
break
case "entryUUID":
ldapUserItem.Uuid = attribute.Values[0]
break
case "mail":
ldapUserItem.Mail = attribute.Values[0]
break
case "email":
ldapUserItem.Email = attribute.Values[0]
break
case "emailAddress":
ldapUserItem.EmailAddress = attribute.Values[0]
break
case "telephoneNumber":
ldapUserItem.TelephoneNumber = attribute.Values[0]
break
case "mobile":
ldapUserItem.Mobile = attribute.Values[0]
break
case "mobileTelephoneNumber":
ldapUserItem.MobileTelephoneNumber = attribute.Values[0]
break
case "registeredAddress":
ldapUserItem.RegisteredAddress = attribute.Values[0]
break
case "postalAddress":
ldapUserItem.PostalAddress = attribute.Values[0]
break
}
}
ldapUsers = append(ldapUsers, ldapUserItem)
}
return ldapUsers, nil
}
func AddLdap(ldap *Ldap) bool {
if len(ldap.Id) == 0 {
ldap.Id = util.GenerateId()
}
if len(ldap.CreatedTime) == 0 {
ldap.CreatedTime = util.GetCurrentTime()
}
affected, err := adapter.Engine.Insert(ldap)
if err != nil {
panic(err)
}
return affected != 0
}
func CheckLdapExist(ldap *Ldap) bool {
var result []*Ldap
err := adapter.Engine.Find(&result, &Ldap{
Owner: ldap.Owner,
Host: ldap.Host,
Port: ldap.Port,
Admin: ldap.Admin,
Passwd: ldap.Passwd,
BaseDn: ldap.BaseDn,
})
if err != nil {
panic(err)
}
if len(result) > 0 {
return true
}
return false
}
func GetLdaps(owner string) []*Ldap {
var ldaps []*Ldap
err := adapter.Engine.Desc("created_time").Find(&ldaps, &Ldap{Owner: owner})
if err != nil {
panic(err)
}
return ldaps
}
func GetLdap(id string) *Ldap {
if util.IsStrsEmpty(id) {
return nil
}
ldap := Ldap{Id: id}
existed, err := adapter.Engine.Get(&ldap)
if err != nil {
panic(err)
}
if existed {
return &ldap
} else {
return nil
}
}
func UpdateLdap(ldap *Ldap) bool {
if GetLdap(ldap.Id) == nil {
return false
}
affected, err := adapter.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "admin", "passwd", "base_dn", "auto_sync").Update(ldap)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteLdap(ldap *Ldap) bool {
affected, err := adapter.Engine.ID(ldap.Id).Delete(&Ldap{})
if err != nil {
panic(err)
}
return affected != 0
}
func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]LdapRespUser) {
var existUsers []LdapRespUser
var failedUsers []LdapRespUser
var uuids []string
for _, user := range users {
uuids = append(uuids, user.Uuid)
}
existUuids := CheckLdapUuidExist(owner, uuids)
for _, user := range users {
if len(existUuids) > 0 {
for index, existUuid := range existUuids {
if user.Uuid == existUuid {
existUsers = append(existUsers, user)
existUuids = append(existUuids[:index], existUuids[index+1:]...)
}
}
}
if !AddUser(&User{
Owner: owner,
Name: buildLdapUserName(user.Uid, user.UidNumber),
CreatedTime: util.GetCurrentTime(),
Password: "123",
DisplayName: user.Cn,
Avatar: "https://casbin.org/img/casbin.svg",
Email: user.Email,
Phone: user.Phone,
Address: []string{user.Address},
Affiliation: "Example Inc.",
Tag: "staff",
Ldap: user.Uuid,
}) {
failedUsers = append(failedUsers, user)
continue
}
}
return &existUsers, &failedUsers
}
func UpdateLdapSyncTime(ldapId string) {
_, err := adapter.Engine.ID(ldapId).Update(&Ldap{LastSync: util.GetCurrentTime()})
if err != nil {
panic(err)
}
}
func CheckLdapUuidExist(owner string, uuids []string) []string {
var results []User
var existUuids []string
//whereStr := ""
//for i, uuid := range uuids {
// if i == 0 {
// whereStr = fmt.Sprintf("'%s'", uuid)
// } else {
// whereStr = fmt.Sprintf(",'%s'", uuid)
// }
//}
err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'" + strings.Join(uuids, "','") + "'"), owner).Find(&results)
if err != nil {
panic(err)
}
if len(results) > 0 {
for _, result := range results {
existUuids = append(existUuids, result.Ldap)
}
}
return existUuids
}
func buildLdapUserName(uid, uidNum string) string {
var result User
uidWithNumber := fmt.Sprintf("%s_%s", uid, uidNum)
has, err := adapter.Engine.Where("name = ? or name = ?", uid, uidWithNumber).Get(&result)
if err != nil {
panic(err)
}
if has {
if result.Name == uid {
return uidWithNumber
}
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6))
}
return uid
}

65
object/record.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casdoor/casdoor/util"
)
type Records struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
Record util.Record `xorm:"extends"`
}
func AddRecord(record *util.Record) bool {
records := new(Records)
records.Record = *record
affected, err := adapter.Engine.Insert(records)
if err != nil {
panic(err)
}
return affected != 0
}
func GetRecordCount() int {
count, err := adapter.Engine.Count(&Records{})
if err != nil {
panic(err)
}
return int(count)
}
func GetRecords() []*Records {
records := []*Records{}
err := adapter.Engine.Desc("id").Find(&records)
if err != nil {
panic(err)
}
return records
}
func GetRecordsByField(record *Records) []*Records {
records := []*Records{}
err := adapter.Engine.Find(&records, record)
if err != nil {
panic(err)
}
return records
}

View File

@ -54,7 +54,9 @@ type User struct {
DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"`
Weibo string `xorm:"weibo varchar(100)" json:"weibo"`
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
}
@ -141,7 +143,9 @@ func UpdateUser(id string, user *User) bool {
user.UpdateUserHash()
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar", "address","language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden", "hash", "properties").Update(user)
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar",
"address", "language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden",
"hash", "properties").Update(user)
if err != nil {
panic(err)
}

View File

@ -1,3 +1,17 @@
// 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 (

70
routers/record.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
import (
"strings"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func getUser(ctx *context.Context) (username string) {
defer func() {
if r := recover(); r != nil {
username = getUserByClientIdSecret(ctx)
}
}()
username = ctx.Input.Session("username").(string)
if username == "" {
username = getUserByClientIdSecret(ctx)
}
return
}
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 {
return ""
}
app := object.GetApplicationByClientId(clientId)
if app == nil || app.ClientSecret != clientSecret {
return ""
}
return app.Organization+"/"+app.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)
}
}

View File

@ -64,6 +64,14 @@ func initAPI() {
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck")
beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps")
beego.Router("/api/get-ldap", &controllers.ApiController{}, "POST:GetLdap")
beego.Router("/api/add-ldap", &controllers.ApiController{}, "POST:AddLdap")
beego.Router("/api/update-ldap", &controllers.ApiController{}, "POST:UpdateLdap")
beego.Router("/api/delete-ldap", &controllers.ApiController{}, "POST:DeleteLdap")
beego.Router("/api/check-ldap-users-exist", &controllers.ApiController{}, "POST:CheckLdapUsersExist")
beego.Router("/api/sync-ldap-users", &controllers.ApiController{}, "POST:SyncLdapUsers")
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")
@ -73,7 +81,7 @@ func initAPI() {
beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications")
beego.Router("/api/get-application", &controllers.ApiController{}, "GET:GetApplication")
beego.Router("/api/get-default-application", &controllers.ApiController{}, "GET:GetDefaultApplication")
beego.Router("/api/get-user-application", &controllers.ApiController{}, "GET:GetUserApplication")
beego.Router("/api/update-application", &controllers.ApiController{}, "POST:UpdateApplication")
beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication")
beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication")
@ -84,4 +92,8 @@ func initAPI() {
beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken")
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
}

View File

@ -362,6 +362,65 @@
}
}
},
"/api/get-default-application": {
"get": {
"tags": [
"api"
],
"description": "get the detail of the default application",
"operationId": "ApiController.GetDefaultApplication",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "The owner of the application.",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Application"
}
}
}
}
},
"/api/get-email-and-phone": {
"post": {
"tags": [
"api"
],
"description": "get email and phone by username",
"operationId": "ApiController.GetEmailAndPhone",
"parameters": [
{
"in": "formData",
"name": "username",
"description": "The username of the user",
"required": true,
"type": "string"
},
{
"in": "formData",
"name": "organization",
"description": "The organization of the user",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/get-global-users": {
"get": {
"tags": [
@ -492,6 +551,57 @@
}
}
},
"/api/get-records": {
"get": {
"tags": [
"api"
],
"description": "get all records",
"operationId": "ApiController.GetRecords",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Records"
}
}
}
}
}
},
"/api/get-records-filter": {
"post": {
"tags": [
"api"
],
"description": "get records by filter",
"operationId": "ApiController.GetRecordsByFilter",
"parameters": [
{
"in": "body",
"name": "body",
"description": "filter Record message",
"required": true,
"schema": {
"$ref": "#/definitions/object.Records"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Records"
}
}
}
}
}
},
"/api/get-token": {
"get": {
"tags": [
@ -700,18 +810,65 @@
}
}
},
"/api/register": {
"/api/set-password": {
"post": {
"tags": [
"api"
],
"description": "register a new user",
"operationId": "ApiController.Register",
"description": "set password",
"operationId": "ApiController.SetPassword",
"parameters": [
{
"in": "formData",
"name": "userOwner",
"description": "The owner of the user",
"required": true,
"type": "string"
},
{
"in": "formData",
"name": "userName",
"description": "The name of the user",
"required": true,
"type": "string"
},
{
"in": "formData",
"name": "oldPassword",
"description": "The old password of the user",
"required": true,
"type": "string"
},
{
"in": "formData",
"name": "newPassword",
"description": "The new password of the user",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/signup": {
"post": {
"tags": [
"api"
],
"description": "sign up a new user",
"operationId": "ApiController.Signup",
"parameters": [
{
"in": "formData",
"name": "username",
"description": "The username to register",
"description": "The username to sign up",
"required": true,
"type": "string"
},
@ -965,7 +1122,7 @@
"tags": [
"api"
],
"description": "register a new user",
"description": "upload avatar",
"operationId": "ApiController.UploadAvatar",
"parameters": [
{
@ -995,7 +1152,11 @@
}
},
"definitions": {
"1471.0xc0003bd890.false": {
"1671.0xc00044ab10.false": {
"title": "false",
"type": "object"
},
"1705.0xc00044ab40.false": {
"title": "false",
"type": "object"
},
@ -1008,7 +1169,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/1471.0xc0003bd890.false"
"$ref": "#/definitions/1671.0xc00044ab10.false"
},
"data2": {
"$ref": "#/definitions/1705.0xc00044ab40.false"
},
"msg": {
"type": "string"
@ -1023,7 +1187,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/1471.0xc0003bd890.false"
"$ref": "#/definitions/1671.0xc00044ab10.false"
},
"data2": {
"$ref": "#/definitions/1705.0xc00044ab40.false"
},
"msg": {
"type": "string"
@ -1037,6 +1204,9 @@
"title": "Application",
"type": "object",
"properties": {
"affiliationUrl": {
"type": "string"
},
"clientId": {
"type": "string"
},
@ -1062,6 +1232,9 @@
"type": "integer",
"format": "int64"
},
"forgetUrl": {
"type": "string"
},
"homepageUrl": {
"type": "string"
},
@ -1074,19 +1247,16 @@
"organization": {
"type": "string"
},
"organizationObj": {
"$ref": "#/definitions/object.Organization"
},
"owner": {
"type": "string"
},
"providerObjs": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Provider"
}
},
"providers": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/object.ProviderItem"
}
},
"redirectUris": {
@ -1094,6 +1264,18 @@
"items": {
"type": "string"
}
},
"signinUrl": {
"type": "string"
},
"signupItems": {
"type": "array",
"items": {
"$ref": "#/definitions/object.SignupItem"
}
},
"signupUrl": {
"type": "string"
}
}
},
@ -1104,15 +1286,30 @@
"createdTime": {
"type": "string"
},
"defaultAvatar": {
"type": "string"
},
"displayName": {
"type": "string"
},
"favicon": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"passwordSalt": {
"type": "string"
},
"passwordType": {
"type": "string"
},
"phonePrefix": {
"type": "string"
},
"websiteUrl": {
"type": "string"
}
@ -1122,32 +1319,121 @@
"title": "Provider",
"type": "object",
"properties": {
"appId": {
"type": "string"
},
"category": {
"type": "string"
},
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"content": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"displayName": {
"type": "string"
},
"host": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int64"
},
"providerUrl": {
"type": "string"
},
"regionId": {
"type": "string"
},
"signName": {
"type": "string"
},
"templateCode": {
"type": "string"
},
"title": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"object.ProviderItem": {
"title": "ProviderItem",
"type": "object",
"properties": {
"alertType": {
"type": "string"
},
"canSignIn": {
"type": "boolean"
},
"canSignUp": {
"type": "boolean"
},
"canUnlink": {
"type": "boolean"
},
"name": {
"type": "string"
},
"prompted": {
"type": "boolean"
},
"provider": {
"$ref": "#/definitions/object.Provider"
}
}
},
"object.Records": {
"title": "Records",
"type": "object",
"properties": {
"Record": {
"$ref": "#/definitions/util.Record"
},
"id": {
"type": "integer",
"format": "int64"
}
}
},
"object.SignupItem": {
"title": "SignupItem",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"prompted": {
"type": "boolean"
},
"required": {
"type": "boolean"
},
"rule": {
"type": "string"
},
"visible": {
"type": "boolean"
}
}
},
"object.Token": {
"title": "Token",
"type": "object",
@ -1171,6 +1457,9 @@
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"owner": {
"type": "string"
},
@ -1179,6 +1468,9 @@
},
"tokenType": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
@ -1205,6 +1497,12 @@
"title": "User",
"type": "object",
"properties": {
"address": {
"type": "array",
"items": {
"type": "string"
}
},
"affiliation": {
"type": "string"
},
@ -1214,27 +1512,45 @@
"createdTime": {
"type": "string"
},
"dingtalk": {
"type": "string"
},
"displayName": {
"type": "string"
},
"email": {
"type": "string"
},
"facebook": {
"type": "string"
},
"gitee": {
"type": "string"
},
"github": {
"type": "string"
},
"google": {
"type": "string"
},
"hash": {
"type": "string"
},
"id": {
"type": "string"
},
"isAdmin": {
"type": "boolean"
},
"isForbidden": {
"type": "boolean"
},
"isGlobalAdmin": {
"type": "boolean"
},
"language": {
"type": "string"
},
"name": {
"type": "string"
},
@ -1244,17 +1560,65 @@
"password": {
"type": "string"
},
"passwordType": {
"type": "string"
},
"phone": {
"type": "string"
},
"preHash": {
"type": "string"
},
"properties": {
"additionalProperties": {
"type": "string"
}
},
"qq": {
"type": "string"
},
"score": {
"type": "integer",
"format": "int64"
},
"signupApplication": {
"type": "string"
},
"tag": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedTime": {
"type": "string"
},
"wechat": {
"type": "string"
},
"weibo": {
"type": "string"
}
}
},
"util.Record": {
"title": "Record",
"type": "object",
"properties": {
"ip": {
"type": "string"
},
"organization": {
"type": "string"
},
"requestTime": {
"type": "string"
},
"requestUri": {
"type": "string"
},
"urlpath": {
"type": "string"
},
"username": {
"type": "string"
}
}
}

View File

@ -234,6 +234,45 @@ paths:
type: array
items:
$ref: '#/definitions/object.Application'
/api/get-default-application:
get:
tags:
- api
description: get the detail of the default application
operationId: ApiController.GetDefaultApplication
parameters:
- in: query
name: owner
description: The owner of the application.
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Application'
/api/get-email-and-phone:
post:
tags:
- api
description: get email and phone by username
operationId: ApiController.GetEmailAndPhone
parameters:
- in: formData
name: username
description: The username of the user
required: true
type: string
- in: formData
name: organization
description: The organization of the user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/get-global-users:
get:
tags:
@ -319,6 +358,39 @@ paths:
type: array
items:
$ref: '#/definitions/object.Provider'
/api/get-records:
get:
tags:
- api
description: get all records
operationId: ApiController.GetRecords
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Records'
/api/get-records-filter:
post:
tags:
- api
description: get records by filter
operationId: ApiController.GetRecordsByFilter
parameters:
- in: body
name: body
description: filter Record message
required: true
schema:
$ref: '#/definitions/object.Records'
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Records'
/api/get-token:
get:
tags:
@ -456,16 +528,48 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/register:
/api/set-password:
post:
tags:
- api
description: register a new user
operationId: ApiController.Register
description: set password
operationId: ApiController.SetPassword
parameters:
- in: formData
name: userOwner
description: The owner of the user
required: true
type: string
- in: formData
name: userName
description: The name of the user
required: true
type: string
- in: formData
name: oldPassword
description: The old password of the user
required: true
type: string
- in: formData
name: newPassword
description: The new password of the user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/signup:
post:
tags:
- api
description: sign up a new user
operationId: ApiController.Signup
parameters:
- in: formData
name: username
description: The username to register
description: The username to sign up
required: true
type: string
- in: formData
@ -633,7 +737,7 @@ paths:
post:
tags:
- api
description: register a new user
description: upload avatar
operationId: ApiController.UploadAvatar
parameters:
- in: formData
@ -652,7 +756,10 @@ paths:
schema:
$ref: '#/definitions/controllers.Response'
definitions:
1471.0xc0003bd890.false:
1671.0xc00044ab10.false:
title: "false"
type: object
1705.0xc00044ab40.false:
title: "false"
type: object
RequestForm:
@ -663,7 +770,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/1471.0xc0003bd890.false'
$ref: '#/definitions/1671.0xc00044ab10.false'
data2:
$ref: '#/definitions/1705.0xc00044ab40.false'
msg:
type: string
status:
@ -673,7 +782,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/1471.0xc0003bd890.false'
$ref: '#/definitions/1671.0xc00044ab10.false'
data2:
$ref: '#/definitions/1705.0xc00044ab40.false'
msg:
type: string
status:
@ -682,6 +793,8 @@ definitions:
title: Application
type: object
properties:
affiliationUrl:
type: string
clientId:
type: string
clientSecret:
@ -699,6 +812,8 @@ definitions:
expireInHours:
type: integer
format: int64
forgetUrl:
type: string
homepageUrl:
type: string
logo:
@ -707,54 +822,130 @@ definitions:
type: string
organization:
type: string
organizationObj:
$ref: '#/definitions/object.Organization'
owner:
type: string
providerObjs:
type: array
items:
$ref: '#/definitions/object.Provider'
providers:
type: array
items:
type: string
$ref: '#/definitions/object.ProviderItem'
redirectUris:
type: array
items:
type: string
signinUrl:
type: string
signupItems:
type: array
items:
$ref: '#/definitions/object.SignupItem'
signupUrl:
type: string
object.Organization:
title: Organization
type: object
properties:
createdTime:
type: string
defaultAvatar:
type: string
displayName:
type: string
favicon:
type: string
name:
type: string
owner:
type: string
passwordSalt:
type: string
passwordType:
type: string
phonePrefix:
type: string
websiteUrl:
type: string
object.Provider:
title: Provider
type: object
properties:
appId:
type: string
category:
type: string
clientId:
type: string
clientSecret:
type: string
content:
type: string
createdTime:
type: string
displayName:
type: string
host:
type: string
name:
type: string
owner:
type: string
port:
type: integer
format: int64
providerUrl:
type: string
regionId:
type: string
signName:
type: string
templateCode:
type: string
title:
type: string
type:
type: string
object.ProviderItem:
title: ProviderItem
type: object
properties:
alertType:
type: string
canSignIn:
type: boolean
canSignUp:
type: boolean
canUnlink:
type: boolean
name:
type: string
prompted:
type: boolean
provider:
$ref: '#/definitions/object.Provider'
object.Records:
title: Records
type: object
properties:
Record:
$ref: '#/definitions/util.Record'
id:
type: integer
format: int64
object.SignupItem:
title: SignupItem
type: object
properties:
name:
type: string
prompted:
type: boolean
required:
type: boolean
rule:
type: string
visible:
type: boolean
object.Token:
title: Token
type: object
@ -772,12 +963,16 @@ definitions:
format: int64
name:
type: string
organization:
type: string
owner:
type: string
scope:
type: string
tokenType:
type: string
user:
type: string
object.TokenWrapper:
title: TokenWrapper
type: object
@ -795,37 +990,85 @@ definitions:
title: User
type: object
properties:
address:
type: array
items:
type: string
affiliation:
type: string
avatar:
type: string
createdTime:
type: string
dingtalk:
type: string
displayName:
type: string
email:
type: string
facebook:
type: string
gitee:
type: string
github:
type: string
google:
type: string
hash:
type: string
id:
type: string
isAdmin:
type: boolean
isForbidden:
type: boolean
isGlobalAdmin:
type: boolean
language:
type: string
name:
type: string
owner:
type: string
password:
type: string
passwordType:
type: string
phone:
type: string
preHash:
type: string
properties:
additionalProperties:
type: string
qq:
type: string
score:
type: integer
format: int64
signupApplication:
type: string
tag:
type: string
type:
type: string
updatedTime:
type: string
wechat:
type: string
weibo:
type: string
util.Record:
title: Record
type: object
properties:
ip:
type: string
organization:
type: string
requestTime:
type: string
requestUri:
type: string
urlpath:
type: string
username:
type: string

View File

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

View File

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

47
util/record.go Normal file
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.
package util
import (
"strings"
"github.com/astaxie/beego/context"
)
type Record struct {
ClientIp string `xorm:"varchar(100)" json:"clientIp"`
Timestamp string `xorm:"varchar(100)" json:"timestamp"`
Organization string `xorm:"varchar(100)" json:"organization"`
Username string `xorm:"varchar(100)" json:"username"`
RequestUri string `xorm:"varchar(1000)" json:"requestUri"`
Action string `xorm:"varchar(1000)" json:"action"`
}
func Records(ctx *context.Context) *Record {
ip := strings.Replace(getIPFromRequest(ctx.Request), ": ", "", -1)
currenttime := GetCurrentTime()
requesturi := ctx.Request.RequestURI
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
record := Record{
ClientIp: ip,
Timestamp: currenttime,
RequestUri: requesturi,
Username: "",
Organization: "",
Action: action,
}
return &record
}

View File

@ -55,3 +55,39 @@ func GetMd5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
func IsStrsEmpty(strs ...string) bool {
r := false
for _, str := range strs {
if len(str) == 0 {
r = true
}
}
return r
}
func GetMaxLenStr(strs ...string) string {
m := 0
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}
func GetMinLenStr(strs ...string) string {
m := int(^uint(0) >> 1)
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}

View File

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

View File

@ -27,8 +27,11 @@ import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage";
import TokenEditPage from "./TokenEditPage";
import RecordListPage from "./RecordListPage";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner";
@ -223,7 +226,7 @@ class App extends Component {
);
return (
<Dropdown key="200" overlay={menu} >
<Dropdown key="200" overlay={menu} className="rightDropDown">
<div className="ant-dropdown-link" style={{float: 'right', cursor: 'pointer'}}>
{
this.renderAvatar()
@ -317,9 +320,16 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="7">
<Link to="/records">
{i18next.t("general:Records")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="6" onClick={() => window.location.href = "/swagger"}>
<Menu.Item key="7" onClick={() => window.location.href = "/swagger"}>
{i18next.t("general:Swagger")}
</Menu.Item>
);
@ -374,6 +384,7 @@ class App extends Component {
{
this.renderAccount()
}
<SelectLanguageBox/>
</Menu>
</Header>
<Switch>
@ -390,8 +401,11 @@ class App extends Component {
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)}/>
<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} />)}/>
</Switch>
</div>
)
@ -409,8 +423,7 @@ class App extends Component {
textAlign: 'center',
}
}>
<SelectLanguageBox/>
Made with <span style={{color: 'rgb(255, 255, 255)'}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casbin.org" rel="noreferrer">Casbin</a>
Made with <span style={{color: 'rgb(255, 255, 255)'}}></span> by <a style={{fontWeight: "bold", color: "black"}} target="_blank" href="https://casdoor.org" rel="noreferrer">Casdoor</a>
</Footer>
)
}

View File

@ -39,3 +39,22 @@
width: 100%;
height: 70px; /* Footer height */
}
.language_box {
background: url("https://cdn.casbin.org/img/muti_language.svg");
background-size: 25px, 25px;
width: 25px;
height: 25px;
margin: 22px 20px 16px 20px;
float: right;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.rightDropDown{
&:hover {
background-color: #f5f5f5;
}
}

View File

@ -289,6 +289,7 @@ class ApplicationEditPage extends React.Component {
title={i18next.t("general:Providers")}
table={this.state.application.providers}
providers={this.state.providers}
application={this.state.application}
onUpdateTable={(value) => { this.updateApplicationField('providers', value)}}
/>
</Col>
@ -301,18 +302,22 @@ class ApplicationEditPage extends React.Component {
this.renderPreview()
}
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} :
</Col>
<Col span={22} >
<SignupTable
title={i18next.t("application:Signup items")}
table={this.state.application.signupItems}
onUpdateTable={(value) => { this.updateApplicationField('signupItems', value)}}
/>
</Col>
</Row>
{
!this.state.application.enableSignUp ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("application:Signup items"), i18next.t("application:Signup items - Tooltip"))} :
</Col>
<Col span={22} >
<SignupTable
title={i18next.t("application:Signup items")}
table={this.state.application.signupItems}
onUpdateTable={(value) => { this.updateApplicationField('signupItems', value)}}
/>
</Col>
</Row>
)
}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :

237
web/src/LdapEditPage.js Normal file
View File

@ -0,0 +1,237 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons";
import * as LddpBackend from "./backend/LdapBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import i18n from "i18next";
const {Option} = Select;
class LdapEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldapId: props.match.params.ldapId,
ldap: null,
organizations: [],
};
}
UNSAFE_componentWillMount() {
this.getLdap();
this.getOrganizations();
}
getLdap() {
LddpBackend.getLdap(this.state.ldapId)
.then((res) => {
if (res.status === "ok") {
this.setState({
ldap: res.data
})
} else {
Setting.showMessage("error", res.msg);
}
})
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
updateLdapField(key, value) {
this.setState((prevState) => {
prevState.ldap[key] = value;
return prevState;
});
}
renderAutoSyncWarn() {
if (this.state.ldap.autoSync > 0) {
return (
<span style={{
color: "#faad14",
marginLeft: "20px"
}}>{i18next.t("ldap:The Auto Sync option will sync all users to specify organization")}</span>
)
}
}
renderLdap() {
return (
<Card size="small" title={
<div>
{i18next.t("ldap:Edit LDAP")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)}
value={this.state.ldap.owner} onChange={(value => {
this.updateLdapField("owner", value);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index}
value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:ID"), i18next.t("general:ID - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.id} disabled={true}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Name"), i18next.t("ldap:Server Name - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.serverName} onChange={e => {
this.updateLdapField("serverName", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Host"), i18next.t("ldap:Server Host - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.host} onChange={e => {
this.updateLdapField("host", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Port"), i18next.t("ldap:Server Port - Tooltip"))} :
</Col>
<Col span={21}>
<InputNumber min={0} max={65535} formatter={value => value.replace(/\$\s?|(,*)/g, "")}
value={this.state.ldap.port} onChange={value => {
this.updateLdapField("port", value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.baseDn} onChange={e => {
this.updateLdapField("baseDn", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.admin} onChange={e => {
this.updateLdapField("admin", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin Password"), i18next.t("ldap:Admin Password - Tooltip"))} :
</Col>
<Col span={21}>
<Input.Password
iconRender={visible => (visible ? <EyeTwoTone/> : <EyeInvisibleOutlined/>)} value={this.state.ldap.passwd}
onChange={e => {
this.updateLdapField("passwd", e.target.value);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :
</Col>
<Col span={21}>
<InputNumber min={0} formatter={value => value.replace(/\$\s?|(,*)/g, "")} disabled={true}
value={this.state.ldap.autoSync} onChange={value => {
this.updateLdapField("autoSync", value);
}}/><span>&nbsp;mins</span>
{this.renderAutoSyncWarn()}
</Col>
</Row>
</Card>
)
}
submitLdapEdit() {
LddpBackend.updateLdap(this.state.ldap)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Update LDAP server success`);
this.setState((prevState) => {
prevState.ldap = res.data2;
})
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Update LDAP server failed: ${error}`);
});
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.state.ldap !== null ? this.renderLdap() : null
}
</Col>
<Col span={1}>
</Col>
</Row>
<Row style={{margin: 10}}>
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large"
onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>
);
}
}
export default LdapEditPage;

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

@ -0,0 +1,193 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, Popconfirm, Row, Table} from "antd";
import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next";
class LdapListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldaps: null
};
}
UNSAFE_componentWillMount() {
this.getLdaps()
}
getLdaps() {
LdapBackend.getLdaps("")
.then((res) => {
let ldapsData = [];
if (res.status === "ok") {
ldapsData = res.data;
} else {
Setting.showMessage("error", res.msg);
}
this.setState((prevState) => {
prevState.ldaps = ldapsData;
return prevState;
})
});
}
deleteLdap(index) {
}
renderTable(ldaps) {
const columns = [
{
title: i18next.t("ldap:Server Name"),
dataIndex: "serverName",
key: "serverName",
width: "200px",
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
render: (text, record, index) => {
return (
<Link to={`/ldaps/${record.id}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "140px",
sorter: (a, b) => a.owner.localeCompare(b.owner),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("ldap:Server"),
dataIndex: "host",
key: "host",
ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => {
return `${text}:${record.port}`
}
},
{
title: i18next.t("ldap:Base DN"),
dataIndex: "baseDn",
key: "baseDn",
ellipsis: true,
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
},
{
title: i18next.t("ldap:Admin"),
dataIndex: "admin",
key: "admin",
ellipsis: true,
sorter: (a, b) => a.admin.localeCompare(b.admin),
},
{
title: i18next.t("ldap:Auto Sync"),
dataIndex: "autoSync",
key: "autoSync",
width: "100px",
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
}
},
{
title: i18next.t("ldap:Last Sync"),
dataIndex: "lastSync",
key: "lastSync",
ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => {
return text
}
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
onConfirm={() => this.deleteLdap(index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
return (
<div>
<Table columns={columns} dataSource={ldaps} rowKey="id" size="middle" bordered
pagination={{pageSize: 100}}
title={() => (
<div>
<span>{i18next.t("general:LDAPs")}</span>
<Button type="primary" size="small" style={{marginLeft: "10px"}}
onClick={() => {
this.addLdap()
}}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={ldaps === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.ldaps)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default LdapListPage;

253
web/src/LdapSyncPage.js Normal file
View File

@ -0,0 +1,253 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Row, Table, Popconfirm} from "antd";
import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next";
class LdapSyncPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldapId: props.match.params.ldapId,
ldap: null,
users: [],
existUuids: [],
selectedUsers: []
};
}
UNSAFE_componentWillMount() {
this.getLdap()
}
syncUsers() {
let selectedUsers = this.state.selectedUsers;
if (selectedUsers === null || selectedUsers.length === 0) {
Setting.showMessage("error", "Please select al least 1 user first");
return
}
LdapBackend.syncUsers(this.state.ldap.owner, this.state.ldap.id, selectedUsers)
.then((res => {
if (res.status === "ok") {
let exist = res.data.exist;
let failed = res.data.failed;
let existUser = [];
let failedUser = [];
if ((!exist || exist.length === 0) && (!failed || failed.length === 0)) {
Setting.goToLink(`/organizations/${this.state.ldap.owner}/users`);
} else {
if (exist && exist.length > 0) {
exist.forEach(elem => {
existUser.push(elem.cn);
});
Setting.showMessage("error", `User [${existUser}] is already exist`);
}
if (failed && failed.length > 0) {
failed.forEach(elem => {
failedUser.push(elem.cn);
})
Setting.showMessage("error", `Sync [${failedUser}] failed`)
}
}
} else {
Setting.showMessage("error", res.msg);
}
}))
}
getLdap() {
LdapBackend.getLdap(this.state.ldapId)
.then((res) => {
if (res.status === "ok") {
this.setState((prevState) => {
prevState.ldap = res.data;
return prevState;
})
this.getLdapUser(res.data);
} else {
Setting.showMessage("error", res.msg);
}
});
}
getLdapUser(ldap) {
LdapBackend.getLdapUser(ldap)
.then((res) => {
if (res.status === "ok") {
this.setState((prevState) => {
prevState.users = res.data.users;
return prevState;
})
this.getExistUsers(ldap.owner, res.data.users);
} else {
Setting.showMessage("error", res.msg);
}
})
}
getExistUsers(owner, users) {
let uuidArray = [];
users.forEach(elem => {
uuidArray.push(elem.uuid);
})
LdapBackend.checkLdapUsersExist(owner, uuidArray)
.then((res) => {
if (res.status === "ok") {
this.setState(prevState => {
prevState.existUuids = res.data?.length > 0 ? res.data : [];
return prevState;
})
}
})
}
buildValArray(data, key) {
let valTypesArray = [];
if (data !== null && data.length > 0) {
data.forEach(elem => {
let val = elem[key];
if (!valTypesArray.includes(val)) {
valTypesArray.push(val);
}
});
}
return valTypesArray;
}
buildFilter(data, key) {
let filterArray = [];
if (data !== null && data.length > 0) {
let valArray = this.buildValArray(data, key)
valArray.forEach(elem => {
filterArray.push({
text: elem,
value: elem,
});
});
}
return filterArray;
}
renderTable(users) {
const columns = [
{
title: i18next.t("ldap:CN"),
dataIndex: "cn",
key: "cn",
sorter: (a, b) => a.cn.localeCompare(b.cn),
},
{
title: i18next.t("ldap:UidNumber / Uid"),
dataIndex: "uidNumber",
key: "uidNumber",
width: "200px",
sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber),
render: (text, record, index) => {
return `${text} / ${record.uid}`
},
},
{
title: i18next.t("ldap:Group Id"),
dataIndex: "groupId",
key: "groupId",
width: "140px",
sorter: (a, b) => a.groupId.localeCompare(b.groupId),
filters: this.buildFilter(this.state.users, "groupId"),
onFilter: (value, record) => record.groupId.indexOf(value) === 0,
},
{
title: i18next.t("ldap:Email"),
dataIndex: "email",
key: "email",
width: "240px",
sorter: (a, b) => a.email.localeCompare(b.email),
},
{
title: i18next.t("ldap:Phone"),
dataIndex: "phone",
key: "phone",
width: "160px",
sorter: (a, b) => a.phone.localeCompare(b.phone),
},
{
title: i18next.t("ldap:Address"),
dataIndex: "address",
key: "address",
sorter: (a, b) => a.address.localeCompare(b.address),
},
];
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState(prevState => {
prevState.selectedUsers = selectedRows;
return prevState;
})
},
getCheckboxProps: record => ({
disabled: this.state.existUuids.indexOf(record.uuid) !== -1,
}),
};
return (
<div>
<Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered
pagination={{pageSize: 100}}
title={() => (
<div>
<span>{this.state.ldap?.serverName}</span>
<Popconfirm placement={"right"}
title={`Please confirm to sync selected users`}
onConfirm={() => this.syncUsers()}
>
<Button type="primary" size="small"
style={{marginLeft: "10px"}}>{i18next.t("ldap:Sync")}</Button>
</Popconfirm>
</div>
)}
loading={users === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.users)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default LdapSyncPage;

201
web/src/LdapTable.js Normal file
View File

@ -0,0 +1,201 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Popconfirm, Row, Table} from 'antd';
import * as Setting from "./Setting";
import i18next from "i18next";
import * as LdapBackend from "./backend/LdapBackend";
import {Link} from "react-router-dom";
class LdapTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
newLdap() {
return {
id: "",
owner: this.props.organizationName,
createdTime: "",
serverName: "Example LDAP Server",
host: "example.com",
port: 389,
admin: "cn=admin,dc=example,dc=com",
passwd: "123",
baseDn: "ou=People,dc=example,dc=com",
autosync: 0,
lastSync: ""
}
}
addRow(table) {
const newLdap = this.newLdap();
LdapBackend.addLdap(newLdap)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Add LDAP server success`);
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, res.data2);
this.updateTable(table);
} else {
Setting.showMessage("error", res.msg);
}
}
)
.catch(error => {
Setting.showMessage("error", `Add LDAP server failed: ${error}`);
});
}
deleteRow(table, i) {
LdapBackend.deleteLdap(table[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Delete LDAP server success`);
table = Setting.deleteRow(table, i);
this.updateTable(table);
} else {
Setting.showMessage("error", res.msg);
}
}
)
.catch(error => {
Setting.showMessage("error", `Delete LDAP server failed: ${error}`);
});
}
renderTable(table) {
const columns = [
{
title: i18next.t("ldap:Server Name"),
dataIndex: "serverName",
key: "serverName",
width: "160px",
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
render: (text, record, index) => {
return (
<Link to={`/ldaps/${record.id}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("ldap:Server"),
dataIndex: "host",
key: "host",
ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => {
return `${text}:${record.port}`
}
},
{
title: i18next.t("ldap:Base DN"),
dataIndex: "baseDn",
key: "baseDn",
ellipsis: true,
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
},
{
title: i18next.t("ldap:Auto Sync"),
dataIndex: "autoSync",
key: "autoSync",
width: "120px",
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
}
},
{
title: i18next.t("ldap:Last Sync"),
dataIndex: "lastSync",
key: "lastSync",
ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => {
return text
}
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
onConfirm={() => this.deleteRow(table, index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
return (
<Table rowKey="id" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small"
onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: '20px'}}>
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
)
}
}
export default LdapTable;

View File

@ -15,9 +15,11 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select} from 'antd';
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable";
const { Option } = Select;
@ -28,11 +30,13 @@ class OrganizationEditPage extends React.Component {
classes: props,
organizationName: props.match.params.organizationName,
organization: null,
ldaps: null,
};
}
UNSAFE_componentWillMount() {
this.getOrganization();
this.getLdaps();
}
getOrganization() {
@ -44,6 +48,21 @@ class OrganizationEditPage extends React.Component {
});
}
getLdaps() {
LdapBackend.getLdaps(this.state.organizationName)
.then(res => {
let resdata = []
if (res.status === "ok") {
if (res.data !== null) {
resdata = res.data;
}
}
this.setState({
ldaps: resdata
})
})
}
parseOrganizationField(key, value) {
// if ([].includes(key)) {
// value = Setting.myParseInt(value);
@ -186,6 +205,20 @@ class OrganizationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}}>
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
</Col>
<Col span={22}>
<LdapTable
title={i18next.t("general:LDAPs")}
table={this.state.ldaps}
organizationName={this.state.organizationName}
onUpdateTable={(value) => {
this.setState({ldaps: value}) }}
/>
</Col>
</Row>
</Card>
)
}
@ -228,7 +261,8 @@ class OrganizationEditPage extends React.Component {
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large" onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
<Button type="primary" size="large"
onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>

View File

@ -120,7 +120,7 @@ class OrganizationListPage extends React.Component {
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
},
{
title: 'Favicon',
title: i18next.t("organization:Favicon"),
dataIndex: 'favicon',
key: 'favicon',
width: '50px',
@ -160,6 +160,19 @@ class OrganizationListPage extends React.Component {
width: '150px',
sorter: (a, b) => a.passwordSalt.localeCompare(b.passwordSalt),
},
{
title: i18next.t("organization:Default avatar"),
dataIndex: 'defaultAvatar',
key: 'defaultAvatar',
width: '50px',
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={40} />
</a>
)
}
},
{
title: i18next.t("general:Action"),
dataIndex: '',

View File

@ -74,7 +74,8 @@ class ProviderEditPage extends React.Component {
{id: 'Facebook', name: 'Facebook'},
{id: 'DingTalk', name: 'DingTalk'},
{id: 'Weibo', name: 'Weibo'},
{id: 'Gitee', name: 'Gitee'}
{id: 'Gitee', name: 'Gitee'},
{id: 'LinkedIn', name: 'LinkedIn'},
]
);
} else if (provider.category === "Email") {

View File

@ -62,7 +62,7 @@ class ProviderTable extends React.Component {
}
renderTable(table) {
const columns = [
let columns = [
{
title: i18next.t("provider:Name"),
dataIndex: 'name',
@ -73,11 +73,11 @@ class ProviderTable extends React.Component {
value={text}
onChange={value => {
this.updateField(table, index, 'name', value);
const provider = this.props.providers.filter(provider => provider.name === value)[0];
const provider = Setting.getArrayItem(this.props.providers, "name", value);
this.updateField(table, index, 'provider', provider);
}} >
{
this.props.providers.filter(provider => table.filter(providerItem => providerItem.name === provider.name).length === 0).map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
Setting.getDeduplicatedArray(this.props.providers, table, "name").map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
}
</Select>
)
@ -89,6 +89,10 @@ class ProviderTable extends React.Component {
key: 'canSignUp',
width: '120px',
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
return null;
}
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'canSignUp', checked);
@ -102,6 +106,10 @@ class ProviderTable extends React.Component {
key: 'canSignIn',
width: '120px',
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
return null;
}
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'canSignIn', checked);
@ -115,6 +123,10 @@ class ProviderTable extends React.Component {
key: 'canUnlink',
width: '120px',
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
return null;
}
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'canUnlink', checked);
@ -128,6 +140,10 @@ class ProviderTable extends React.Component {
key: 'prompted',
width: '120px',
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
return null;
}
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'prompted', checked);
@ -135,27 +151,27 @@ class ProviderTable extends React.Component {
)
}
},
{
title: i18next.t("provider:alertType"),
dataIndex: 'alertType',
key: 'alertType',
width: '120px',
render: (text, record, index) => {
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'alertType', value);
})}>
{
[
{id: 'None', name: 'None'},
{id: 'Once', name: 'Once'},
{id: 'Always', name: 'Always'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
}
},
// {
// title: i18next.t("provider:alertType"),
// dataIndex: 'alertType',
// key: 'alertType',
// width: '120px',
// render: (text, record, index) => {
// return (
// <Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
// this.updateField(table, index, 'alertType', value);
// })}>
// {
// [
// {id: 'None', name: 'None'},
// {id: 'Once', name: 'Once'},
// {id: 'Always', name: 'Always'},
// ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
// }
// </Select>
// )
// }
// },
{
title: i18next.t("general:Action"),
key: 'action',
@ -178,8 +194,12 @@ class ProviderTable extends React.Component {
},
];
if (!this.props.application.enableSignUp || this.props.application.enablePassword) {
columns = columns.filter(column => column.key !== "canSignUp");
}
return (
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
<Table rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;

159
web/src/RecordListPage.js Normal file
View File

@ -0,0 +1,159 @@
// 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 {Col, Row, Table} from 'antd';
import * as Setting from "./Setting";
import * as RecordBackend from "./backend/RecordBackend";
import i18next from "i18next";
class RecordListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
records: null,
};
}
UNSAFE_componentWillMount() {
this.getRecords();
}
getRecords() {
RecordBackend.getRecords()
.then((res) => {
this.setState({
records: res,
});
});
}
newRecord() {
return {
id : "",
Record:{
clientIp:"",
timestamp:"",
organization:"",
username:"",
requestUri:"",
action:"login",
},
}
}
renderTable(records) {
const columns = [
{
title: i18next.t("general:Client ip"),
dataIndex: ['Record', 'clientIp'],
key: 'id',
width: '120px',
sorter: (a, b) => a.Record.clientIp.localeCompare(b.Record.clientIp),
render: (text, record, index) => {
return text;
}
},
{
title: i18next.t("general:Timestamp"),
dataIndex: ['Record', 'timestamp'],
key: 'id',
width: '160px',
sorter: (a, b) => a.Record.timestamp.localeCompare(b.Record.timestamp),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Organization"),
dataIndex: ['Record', 'organization'],
key: 'id',
width: '120px',
sorter: (a, b) => a.Record.organization.localeCompare(b.Record.organization),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Username"),
dataIndex: ['Record', 'username'],
key: 'id',
width: '160px',
sorter: (a, b) => a.Record.username.localeCompare(b.Record.username),
render: (text, record, index) => {
return text;
}
},
{
title: i18next.t("general:Request uri"),
dataIndex: ['Record', 'requestUri'],
key: 'id',
width: '160px',
sorter: (a, b) => a.Record.requestUri.localeCompare(b.Record.requestUri),
render: (text, record, index) => {
return text;
}
},
{
title: i18next.t("general:Action"),
dataIndex: ['Record', 'action'],
key: 'id',
width: '160px',
sorter: (a, b) => a.Record.action.localeCompare(b.Record.action),
render: (text, record, index) => {
return text;
}
},
];
return (
<div>
<Table columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{i18next.t("general:Records")}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
)}
loading={records === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.records)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default RecordListPage;

View File

@ -14,6 +14,13 @@
import React from "react";
import * as Setting from "./Setting";
import { Menu, Dropdown, message } from "antd";
import { createFromIconfontCN } from '@ant-design/icons';
import './App.less';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2680620_ffij16fkwdg.js',
});
class SelectLanguageBox extends React.Component {
constructor(props) {
@ -23,47 +30,28 @@ class SelectLanguageBox extends React.Component {
};
}
onClick(e) {
Setting.changeLanguage(e.key);
};
render() {
const menu = (
<Menu onClick={this.onClick.bind(this)}>
<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>
<Menu.Item key="de" icon={<IconFont type="icon-de" />}>Deutsch</Menu.Item>
<Menu.Item key="ja" icon={<IconFont type="icon-ja" />}>日本語</Menu.Item>
<Menu.Item key="ko" icon={<IconFont type="icon-ko" />}>한국어</Menu.Item>
<Menu.Item key="ru" icon={<IconFont type="icon-ru" />}>Русский</Menu.Item>
</Menu>
);
return (
<div align="center">
<div className="box" style={{width: "600px"}}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("en")} className="lang-selector">
English
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("zh")} className="lang-selector">
简体中文
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("fr")} className="lang-selector">
Français
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("de")} className="lang-selector">
Deutsch
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("ja")} className="lang-selector">
日本語
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("ko")} className="lang-selector">
한국어
</a>
/
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => Setting.changeLanguage("ru")} className="lang-selector">
Русский
</a>
</div>
</div>
)
<Dropdown overlay={menu} style={{cursor: "pointer"}}>
<span className="language_box" />
</Dropdown>
);
}
}

View File

@ -448,3 +448,13 @@ export function maskEmail(email) {
return `${username}@${domainTokens.join(".")}`;
}
export function getArrayItem(array, key, value) {
const res = array.filter(item => item[key] === value)[0];
return res;
}
export function getDeduplicatedArray(array, filterArray, key) {
const res = array.filter(item => filterArray.filter(filterItem => filterItem[key] === item[key]).length === 0);
return res;
}

View File

@ -68,6 +68,18 @@ class SignupTable extends React.Component {
dataIndex: 'name',
key: 'name',
render: (text, record, index) => {
const items = [
{id: 'Username', name: 'Username'},
{id: 'ID', name: 'ID'},
{id: 'Display name', name: 'Display name'},
{id: 'Affiliation', name: 'Affiliation'},
{id: 'Email', name: 'Email'},
{id: 'Password', name: 'Password'},
{id: 'Confirm password', name: 'Confirm password'},
{id: 'Phone', name: 'Phone'},
{id: 'Agreement', name: 'Agreement'},
];
return (
<Select virtual={false} style={{width: '100%'}}
value={text}
@ -75,17 +87,7 @@ class SignupTable extends React.Component {
this.updateField(table, index, 'name', value);
}} >
{
[
{id: 'Username', name: 'Username'},
{id: 'ID', name: 'ID'},
{id: 'Display name', name: 'Display name'},
{id: 'Affiliation', name: 'Affiliation'},
{id: 'Email', name: 'Email'},
{id: 'Password', name: 'Password'},
{id: 'Confirm password', name: 'Confirm password'},
{id: 'Phone', name: 'Phone'},
{id: 'Agreement', name: 'Agreement'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
}
</Select>
)
@ -97,6 +99,10 @@ class SignupTable extends React.Component {
key: 'visible',
width: '120px',
render: (text, record, index) => {
if (record.name === "ID") {
return null;
}
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'visible', checked);
@ -132,6 +138,10 @@ class SignupTable extends React.Component {
key: 'prompted',
width: '120px',
render: (text, record, index) => {
if (record.name === "ID") {
return null;
}
if (record.visible) {
return null;
}
@ -149,17 +159,29 @@ class SignupTable extends React.Component {
key: 'rule',
width: '120px',
render: (text, record, index) => {
let options = [];
if (record.name === "ID") {
options = [
{id: 'Random', name: 'Random'},
{id: 'Incremental', name: 'Incremental'},
];
} if (record.name === "Display name") {
options = [
{id: 'None', name: 'None'},
{id: 'Personal', name: 'Personal'},
];
}
if (options.length === 0) {
return null;
}
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'rule', value);
})}>
{
[
{id: 'None', name: 'None'},
{id: 'Random', name: 'Random'},
{id: 'Incremental', name: 'Incremental'},
{id: 'Personal', name: 'Personal'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
@ -188,7 +210,7 @@ class SignupTable extends React.Component {
];
return (
<Table rowKey="index" columns={columns} dataSource={table} size="middle" bordered pagination={false}
<Table rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -49,7 +49,7 @@ class UserEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getUser();
this.getOrganizations();
this.getDefaultApplication();
this.getUserApplication();
}
getUser() {
@ -70,8 +70,8 @@ class UserEditPage extends React.Component {
});
}
getDefaultApplication() {
ApplicationBackend.getDefaultApplication("admin")
getUserApplication() {
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
.then((application) => {
this.setState({
application: application,

View File

@ -146,7 +146,7 @@ class UserListPage extends React.Component {
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
// width: '100px',
width: '100px',
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
},
{
@ -245,6 +245,7 @@ class UserListPage extends React.Component {
dataIndex: '',
key: 'op',
width: '190px',
fixed: 'right',
render: (text, record, index) => {
return (
<div>
@ -263,7 +264,7 @@ class UserListPage extends React.Component {
return (
<div>
<Table columns={columns} dataSource={users} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
<Table columns={columns} scroll={{x: 1300}} dataSource={users} rowKey="name" size="middle" bordered pagination={{pageSize: 100}}
title={() => (
<div>
{i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -278,7 +278,7 @@ class LoginPage extends React.Component {
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
<Checkbox style={{float: "left"}} disabled={!application.enablePassword}>
{i18next.t("login:Auto login")}
</Checkbox>

View File

@ -13,7 +13,6 @@
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, Result, Row} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as UserBackend from "../backend/UserBackend";
@ -196,13 +195,11 @@ class PromptPage extends React.Component {
title="Sign Up Error"
subTitle={"You are unexpected to see this prompt page"}
extra={[
<Link onClick={() => {
<Button type="primary" key="signin" onClick={() => {
Setting.goToLogin(this, application);
}}>
<Button type="primary" key="signin">
Sign In
</Button>
</Link>
Sign In
</Button>
]}
>
</Result>
@ -227,7 +224,7 @@ class PromptPage extends React.Component {
</Col>
</Row>
<div style={{marginTop: "50px"}}>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true)}}>{i18next.t("signup:Submit and complete")}</Button>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true)}}>{i18next.t("code:Submit and complete")}</Button>
</div>
</div>
</Col>

View File

@ -47,6 +47,10 @@ const GiteeAuthScope = "user_info,emails";
const GiteeAuthUri = "https://gitee.com/oauth/authorize";
const GiteeAuthLogo = `${StaticBaseUrl}/img/social_gitee.png`;
const LinkedInAuthScope = "r_liteprofile%20r_emailaddress";
const LinkedInAuthUri = "https://www.linkedin.com/oauth/v2/authorization";
const LinkedInAuthLogo = `${StaticBaseUrl}/img/social_linkedin.png`;
export function getAuthLogo(provider) {
if (provider.type === "Google") {
return GoogleAuthLogo;
@ -64,6 +68,8 @@ export function getAuthLogo(provider) {
return WeiboAuthLogo;
} else if (provider.type === "Gitee") {
return GiteeAuthLogo;
} else if (provider.type === "LinkedIn") {
return LinkedInAuthLogo;
}
}
@ -90,5 +96,7 @@ export function getAuthUrl(application, provider, method) {
return `${WeiboAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${WeiboAuthScope}&response_type=code&state=${state}`;
} 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}`
}
}

View File

@ -161,6 +161,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="username"
key="username"
label={i18next.t("signup:Username")}
rules={[
{
@ -177,6 +178,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="name"
key="name"
label={signupItem.rule === "Personal" ? i18next.t("general:Personal name") : i18next.t("general:Display name")}
rules={[
{
@ -193,6 +195,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="affiliation"
key="affiliation"
label={i18next.t("user:Affiliation")}
rules={[
{
@ -210,6 +213,7 @@ class SignupPage extends React.Component {
<React.Fragment>
<Form.Item
name="email"
key="email"
label={i18next.t("general:Email")}
rules={[
{
@ -233,6 +237,7 @@ class SignupPage extends React.Component {
</Form.Item>
<Form.Item
name="emailCode"
key="emailCode"
label={i18next.t("code:Email code")}
rules={[{
required: required,
@ -253,6 +258,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="password"
key="password"
label={i18next.t("general:Password")}
rules={[
{
@ -269,6 +275,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="confirm"
key="confirm"
label={i18next.t("signup:Confirm")}
dependencies={['password']}
hasFeedback
@ -296,6 +303,7 @@ class SignupPage extends React.Component {
<React.Fragment>
<Form.Item
name="phone"
key="phone"
label={i18next.t("general:Phone")}
rules={[
{
@ -325,6 +333,7 @@ class SignupPage extends React.Component {
</Form.Item>
<Form.Item
name="phoneCode"
key="phoneCode"
label={i18next.t("code:Phone code")}
rules={[
{
@ -347,6 +356,7 @@ class SignupPage extends React.Component {
return (
<Form.Item
name="agreement"
key="agreement"
valuePropName="checked"
rules={[
{
@ -375,13 +385,11 @@ class SignupPage extends React.Component {
title="Sign Up Error"
subTitle={"The application does not allow to sign up new account"}
extra={[
<Link onClick={() => {
<Button type="primary" key="signin" onClick={() => {
Setting.goToLogin(this, application);
}}>
<Button type="primary" key="signin">
Sign In
</Button>
</Link>
Sign In
</Button>
]}
>
</Result>
@ -431,11 +439,11 @@ class SignupPage extends React.Component {
{i18next.t("account:Sign Up")}
</Button>
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<Link onClick={() => {
<a onClick={() => {
Setting.goToLogin(this, application);
}}>
{i18next.t("signup:sign in now")}
</Link>
</a>
</Form.Item>
</Form>
)

View File

@ -28,8 +28,8 @@ export function getApplication(owner, name) {
}).then(res => res.json());
}
export function getDefaultApplication(owner) {
return fetch(`${Setting.ServerUrl}/api/get-default-application?owner=${owner}`, {
export function getUserApplication(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-user-application?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());

View File

@ -0,0 +1,77 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getLdaps(owner) {
return fetch(`${Setting.ServerUrl}/api/get-ldaps?owner=${owner}`, {
method: "POST",
credentials: "include",
}).then(res => res.json());
}
export function getLdap(id) {
return fetch(`${Setting.ServerUrl}/api/get-ldap?id=${id}`, {
method: "POST",
credentials: "include",
}).then(res => res.json());
}
export function addLdap(body) {
return fetch(`${Setting.ServerUrl}/api/add-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function deleteLdap(body) {
return fetch(`${Setting.ServerUrl}/api/delete-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function updateLdap(body) {
return fetch(`${Setting.ServerUrl}/api/update-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function getLdapUser(body) {
return fetch(`${Setting.ServerUrl}/api/get-ldap-user`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function syncUsers(owner, ldapId, body) {
return fetch(`${Setting.ServerUrl}/api/sync-ldap-users?owner=${owner}&ldapId=${ldapId}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function checkLdapUsersExist(owner, body) {
return fetch(`${Setting.ServerUrl}/api/check-ldap-users-exist?owner=${owner}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}

View File

@ -0,0 +1,22 @@
// 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 getRecords() {
return fetch(`${Setting.ServerUrl}/api/get-records`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}

View File

@ -79,7 +79,7 @@ class HomePage extends React.Component {
{
items.map(item => {
return (
<SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.organizer} time={item.createdTime} isSingle={items.length === 1} />
<SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.organizer} time={item.createdTime} isSingle={items.length === 1} key={item.name} />
)
})
}

View File

@ -1,3 +1,17 @@
// 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 {Cascader, Col, Input, Row, Select} from 'antd';
import i18next from "i18next";
@ -87,7 +101,8 @@ class AffiliationSelect extends React.Component {
) : (
<Select virtual={false} style={{width: '100%'}} value={this.props.user.affiliation} onChange={(value => {
const name = value;
const id = this.state.affiliationOptions.filter(affiliationOption => affiliationOption.name === name)[0].id;
const affiliationOption = Setting.getArrayItem(this.state.affiliationOptions, "name", name);
const id = affiliationOption.id;
this.updateUserField('affiliation', name);
this.updateUserField('score', id);
})}>

View File

@ -30,9 +30,11 @@ export const CountDownInput = (props) => {
const [checkType, setCheckType] = React.useState("");
const [coolDown, setCoolDown] = React.useState(false);
const [checkId, setCheckId] = React.useState("");
const [buttonDisabled, setButtonDisabled] = React.useState(false);
const countDown = (leftTime) => {
if (leftTime === 0) {
setButtonDisabled(false);
setCoolDown(false);
setButtonText(defaultButtonText);
return;
@ -41,20 +43,13 @@ export const CountDownInput = (props) => {
setTimeout(() => countDown(leftTime - 1), 1000);
}
const clickButton = () => {
if (coolDown) {
Setting.showMessage("error", i18next.t("general:Cooling down"));
return;
}
loadHumanCheck();
}
const handleOk = () => {
setVisible(false);
onButtonClick(checkType, checkId, key, ...onButtonClickArgs).then(res => {
setKey("");
if (res) {
setCoolDown(true);
setButtonDisabled(true)
countDown(coolDownTime);
}
})
@ -124,13 +119,13 @@ export const CountDownInput = (props) => {
placeholder={placeHolder}
onChange={e => onChange(e.target.value)}
enterButton={
<Button type={"primary"} disabled={disabled}>
<Button type={"primary"} disabled={disabled || buttonDisabled}>
<div style={{fontSize: 14}}>
{buttonText}
</div>
</Button>
}
onSearch={clickButton}
onSearch={loadHumanCheck}
/>
<Modal
closable={false}

View File

@ -12,6 +12,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -267,5 +273,28 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -13,6 +13,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -277,5 +283,29 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap":
{
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -12,6 +12,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -267,5 +273,28 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -12,6 +12,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -267,5 +273,28 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -12,6 +12,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -267,5 +273,28 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -12,6 +12,12 @@
"User": "User",
"Applications": "Applications",
"Application": "Application",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -267,5 +273,28 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -5,13 +5,19 @@
"Organizations": "组织",
"Organizations - Tooltip": "Unique string-style identifier",
"Organization": "组织",
"Organization - Tooltip": "Unique string-style identifier",
"Organization - Tooltip": "用户所属组织",
"Providers": "提供商",
"Providers - Tooltip": "第三方登录需要配置的提供方",
"Providers - Tooltip": "可用于登录的第三方应用程序列表",
"Users": "用户",
"User": "用户",
"Applications": "应用",
"Application": "应用",
"Records": "Records",
"Client ip": "Client ip",
"Timestamp": "Timestamp",
"Username": "用户名",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "保存",
"Add": "添加",
"Action": "操作",
@ -42,9 +48,9 @@
"Password salt - Tooltip": "用于密码加密的随机参数",
"Password": "密码",
"Email": "电子邮箱",
"Email - Tooltip": "电子邮件:",
"Email - Tooltip": "email",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone - Tooltip": "Phone",
"Logo": "图标",
"Logo - Tooltip": "应用程序向外展示的图标",
"User containers": "用户容器",
@ -53,14 +59,14 @@
"Applications that requires authentication": "需要鉴权的应用",
"Swagger": "API文档",
"Phone Prefix": "手机号前缀",
"Phone Prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Phone Prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Enter the code": "输入验证码",
"Captcha": "人机验证码",
"Authorization code": "授权码",
"Access token": "访问令牌",
"Expires in": "有效期",
"Scope": "范围",
"Description": "Description",
"Description": "描述",
"Description - Tooltip": "与此有关的描述信息",
"Token expire": "Token expire",
"Token expire - Tooltip": "签发的令牌的授权时间",
@ -100,7 +106,7 @@
"Please input your verification code!": "请输入您的验证码",
"Send Code": "发送验证码",
"Empty Code": "验证码为空",
"Code Sent": "验证码已发送",
"Code Sent": "Code Sent",
"Code You Received": "验证码",
"Enter your code": "输入你的验证码",
"You can only send one code in 60s.": "每分钟你只能发送一次验证码",
@ -141,36 +147,36 @@
"organization": {
"Edit Organization": "修改组织",
"Website URL": "网页地址",
"Website URL - Tooltip": "唯一的、字符串式的ID"
"Website URL - Tooltip": "Unique string-style identifier"
},
"provider": {
"App ID": "应用ID",
"App ID - Tooltip": "唯一的、字符串式的ID",
"App ID": "App ID",
"App ID - Tooltip": "Unique string-style identifier",
"Name": "名称",
"Display name": "显示名称",
"Category": "分类",
"Category - Tooltip": "唯一的、字符串式的ID",
"Category - Tooltip": "Unique string-style identifier",
"Type": "类型",
"Type - Tooltip": "唯一的、字符串式的ID",
"Type - Tooltip": "Unique string-style identifier",
"Client ID": "Client ID",
"Client ID - Tooltip": "唯一的、字符串式的ID",
"Client ID - Tooltip": "Unique string-style identifier",
"Client secret": "Client secret",
"Client secret - Tooltip": "唯一的、字符串式的ID",
"Client secret - Tooltip": "Unique string-style identifier",
"Host": "主机",
"Host - Tooltip": "唯一的、字符串式的ID",
"Host - Tooltip": "Unique string-style identifier",
"Port": "端口号",
"Port - Tooltip": "唯一的、字符串式的ID",
"Port - Tooltip": "Unique string-style identifier",
"Email Title": "邮件标题",
"Email Title - Tooltip": "唯一的、字符串式的ID",
"Email Title - Tooltip": "Unique string-style identifier",
"Email Content": "邮件内容",
"Email Content - Tooltip": "唯一的、字符串式的ID",
"Email Content - Tooltip": "Unique string-style identifier",
"Region ID": "地域ID",
"Sign Name": "签名名称",
"Sign Name - Tooltip": "唯一的、字符串式的ID",
"Sign Name - Tooltip": "Unique string-style identifier",
"Template Code": "模板CODE",
"Template Code - Tooltip": "唯一的、字符串式的ID",
"Template Code - Tooltip": "Unique string-style identifier",
"Provider URL": "提供商URL",
"Provider URL - Tooltip": "唯一的、字符串式的ID",
"Provider URL - Tooltip": "Unique string-style identifier",
"Edit Provider": "修改提供商"
},
"user": {
@ -181,12 +187,12 @@
"Set password...": "设置密码...",
"Modify password...": "修改密码...",
"Address": "地址",
"Address - Tooltip": "唯一的、字符串式的ID",
"Address - Tooltip": "Unique string-style identifier",
"Affiliation": "工作单位",
"Affiliation - Tooltip": "唯一的、字符串式的ID",
"Affiliation - Tooltip": "Unique string-style identifier",
"Modify affiliation": "修改工作单位",
"Tag": "标签",
"Tag - Tooltip": "唯一的、字符串式的ID",
"Tag - Tooltip": "Unique string-style identifier",
"Third-party logins": "第三方登录",
"Third-party logins - Tooltip": "使用第三方应用程序登录",
"Properties": "属性",
@ -195,9 +201,9 @@
"Is admin": "是管理员",
"Is admin - Tooltip": "是应用程序管理员",
"Is global admin": "是全局管理员",
"Is global admin - Tooltip": "是应用程序管理员",
"Is global admin - Tooltip": "Is the application global administrator",
"Is forbidden": "被禁用",
"Is forbidden - Tooltip": "账户是否已被禁用",
"Is forbidden - Tooltip": "Whether the account is disabled",
"Empty input!": "输入为空!",
"Two passwords you typed do not match.": "两次输入的密码不匹配。",
"Password Set": "密码已设置",
@ -234,8 +240,8 @@
"application": {
"Edit Application": "修改应用",
"Password ON": "Password ON",
"Password ON - Tooltip": "是否允许密码登录",
"Enable signup": "启用注册",
"Password ON - Tooltip": "Whether to allow password login",
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "是否允许用户注册",
"Login page preview": "登录页面预览",
"Test signup page..": "测试注册页面..",
@ -245,7 +251,7 @@
"Redirect URLs": "回调URLs",
"Redirect URLs - Tooltip": "登录成功后重定向地址列表",
"Signup items": "注册项",
"Signup items - Tooltip": "注册用户注册时需要填写的项目"
"Signup items - Tooltip": "Signup items that need to be filled in when users register"
},
"forget": {
"Please input your application!": "请输入您的应用名称!",
@ -267,5 +273,28 @@
"Email/Phone": "邮箱/手机号",
"Change Password": "修改密码",
"Choose email verification or mobile verification": "选择邮箱验证或手机验证"
},
"ldap": {
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -1217,6 +1217,13 @@
semver "^7.3.2"
webpack-merge "^4.2.2"
"@crowdin/cli@^3.6.4":
version "3.6.4"
resolved "https://registry.yarnpkg.com/@crowdin/cli/-/cli-3.6.4.tgz#37e872fbbc85fdfb55e8deba4e35d4393a802aad"
integrity sha512-kFqTf1dFhtSMfF4YWyJ6EAKjjtBEVtHhX2f9cBYc8I51KMTFIOzqeArKFL2qhhq/1Ition2wuE1z7ySwA341+w==
dependencies:
shelljs "^0.8.4"
"@csstools/convert-colors@^1.4.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
@ -5489,6 +5496,18 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
dependencies:
is-glob "^4.0.1"
glob@^7.0.0:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
@ -6073,6 +6092,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@ -10047,6 +10071,13 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
dependencies:
resolve "^1.1.6"
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@ -10302,9 +10333,9 @@ resolve@1.18.1:
is-core-module "^2.0.0"
path-parse "^1.0.6"
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
dependencies:
is-core-module "^2.2.0"
@ -10706,6 +10737,15 @@ shell-quote@1.7.2:
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
shelljs@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
rechoir "^0.6.2"
shellwords@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"