Compare commits

..

11 Commits

Author SHA1 Message Date
hanpeng
a111fd672c fix: Add the configuration of whether to print SQL. The default value is false (#429) 2022-01-19 16:58:45 +08:00
Gucheng Wang
9fd175eefd Add ErrorText to syncer. 2022-01-17 21:17:42 +08:00
Gucheng Wang
d9bcce9485 Start syncer dynamically. 2022-01-17 20:09:29 +08:00
Gucheng Wang
d183b9eca9 Change syncer.SyncInterval to second-level. 2022-01-17 19:27:52 +08:00
Gucheng Wang
f24d9ae251 Don't update password in AddUsers(). 2022-01-17 13:26:30 +08:00
Gucheng Wang
030c1caa50 Fix bug in IsGlobalAdmin(). 2022-01-15 23:23:14 +08:00
Gucheng Wang
cee2c608a2 Disable PasswordModal when needed in user edit page. 2022-01-15 21:34:37 +08:00
Gucheng Wang
82d0e895e0 Update users and roles when org is changed in permission edit page. 2022-01-15 21:11:47 +08:00
Gucheng Wang
dee9bac110 Show signupApplication in user edit page. 2022-01-15 18:29:10 +08:00
Gucheng Wang
e7a6986b62 Add index to User.Id 2022-01-14 17:42:11 +08:00
Gucheng Wang
b91b4aec91 Allow global admin to modify username. 2022-01-13 23:20:10 +08:00
28 changed files with 243 additions and 66 deletions

View File

@@ -6,6 +6,7 @@ copyrequestbody = true
driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/
dbName = casdoor
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false

View File

@@ -38,8 +38,17 @@ func (c *ApiController) GetApplications() {
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
organization := c.Input().Get("organization")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedApplications(object.GetApplications(owner), userId)
var applications []*object.Application
if organization == "" {
applications = object.GetApplications(owner)
} else {
applications = object.GetApplicationsByOrganizationName(owner, organization)
}
c.Data["json"] = object.GetMaskedApplications(applications, userId)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)

View File

@@ -15,9 +15,11 @@
package controllers
import (
"strings"
"time"
"github.com/astaxie/beego"
"github.com/casbin/casdoor/object"
"github.com/casbin/casdoor/util"
)
@@ -35,6 +37,21 @@ type SessionData struct {
ExpireTime int64
}
func (c *ApiController) IsGlobalAdmin() bool {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {
// e.g., "app/app-casnode"
return true
}
user := object.GetUser(username)
if user == nil {
return false
}
return user.Owner == "built-in" || user.IsGlobalAdmin
}
// GetSessionUsername ...
func (c *ApiController) GetSessionUsername() string {
// check if user session expired

View File

@@ -202,7 +202,7 @@ func (c *ApiController) UploadResource() {
}
user.Avatar = fileUrl
object.UpdateUser(user.GetId(), user, []string{"avatar"})
object.UpdateUser(user.GetId(), user, []string{"avatar"}, false)
case "termsOfUse":
applicationId := fmt.Sprintf("admin/%s", parent)
app := object.GetApplication(applicationId)

View File

@@ -125,7 +125,8 @@ func (c *ApiController) UpdateUser() {
columns = strings.Split(columnsStr, ",")
}
affected := object.UpdateUser(id, &user, columns)
isGlobalAdmin := c.IsGlobalAdmin()
affected := object.UpdateUser(id, &user, columns, isGlobalAdmin)
if affected {
object.UpdateUserToOriginalDatabase(&user)
}

2
go.mod
View File

@@ -18,9 +18,9 @@ require (
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8
github.com/mileusna/crontab v1.0.1
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0
github.com/russellhaering/goxmldsig v1.1.1
github.com/satori/go.uuid v1.2.0 // indirect

4
go.sum
View File

@@ -264,8 +264,6 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg=
github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -313,6 +311,8 @@ github.com/qiangmzsx/string-adapter/v2 v2.1.0 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbO
github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA=
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 h1:J2Xj92efYLxPl3BiibgEDEUiMsCBzwTurE/8JjD8CG4=
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=

View File

@@ -110,6 +110,9 @@ func (a *Adapter) close() {
}
func (a *Adapter) createTable() {
showSql, _ := beego.AppConfig.Bool("showSql")
a.Engine.ShowSQL(showSql)
err := a.Engine.Sync2(new(Organization))
if err != nil {
panic(err)

View File

@@ -89,7 +89,7 @@ func GetPaginationApplications(owner string, offset, limit int, field, value, so
return applications
}
func getApplicationsByOrganizationName(owner string, organization string) []*Application {
func GetApplicationsByOrganizationName(owner string, organization string) []*Application {
applications := []*Application{}
err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner, Organization: organization})
if err != nil {

View File

@@ -124,7 +124,7 @@ func UpdateOrganization(id string, organization *Organization) bool {
}
if name != organization.Name {
applications := getApplicationsByOrganizationName("admin", name)
applications := GetApplicationsByOrganizationName("admin", name)
for _, application := range applications {
application.Organization = organization.Name
UpdateApplication(application.GetId(), application)

View File

@@ -48,6 +48,7 @@ type Syncer struct {
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
AffiliationTable string `xorm:"varchar(100)" json:"affiliationTable"`
AvatarBaseUrl string `xorm:"varchar(100)" json:"avatarBaseUrl"`
ErrorText string `xorm:"mediumtext" json:"errorText"`
SyncInterval int `json:"syncInterval"`
IsEnabled bool `json:"isEnabled"`
@@ -140,6 +141,26 @@ func UpdateSyncer(id string, syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
addSyncerJob(syncer)
}
return affected != 0
}
func updateSyncerErrorText(syncer *Syncer, line string) bool {
s := getSyncer(syncer.Owner, syncer.Name)
if s == nil {
return false
}
s.ErrorText = s.ErrorText + line
affected, err := adapter.Engine.ID(core.PK{s.Owner, s.Name}).Cols("error_text").Update(s)
if err != nil {
panic(err)
}
return affected != 0
}
@@ -149,6 +170,10 @@ func AddSyncer(syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
addSyncerJob(syncer)
}
return affected != 0
}
@@ -158,6 +183,10 @@ func DeleteSyncer(syncer *Syncer) bool {
panic(err)
}
if affected == 1 {
deleteSyncerJob(syncer)
}
return affected != 0
}

View File

@@ -14,27 +14,56 @@
package object
import "github.com/mileusna/crontab"
import (
"fmt"
var cronMap map[string]*crontab.Crontab
"github.com/robfig/cron/v3"
)
var cronMap map[string]*cron.Cron
func init() {
cronMap = map[string]*crontab.Crontab{}
cronMap = map[string]*cron.Cron{}
}
func getCrontab(name string) *crontab.Crontab {
ctab, ok := cronMap[name]
func getCronMap(name string) *cron.Cron {
m, ok := cronMap[name]
if !ok {
ctab = crontab.New()
cronMap[name] = ctab
m = cron.New()
cronMap[name] = m
}
return ctab
return m
}
func clearCrontab(name string) {
ctab, ok := cronMap[name]
func clearCron(name string) {
cron, ok := cronMap[name]
if ok {
ctab.Clear()
cron.Stop()
delete(cronMap, name)
}
}
func addSyncerJob(syncer *Syncer) {
deleteSyncerJob(syncer)
if !syncer.IsEnabled {
return
}
syncer.initAdapter()
syncer.syncUsers()
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
cron := getCronMap(syncer.Name)
_, err := cron.AddFunc(schedule, syncer.syncUsers)
if err != nil {
panic(err)
}
cron.Start()
}
func deleteSyncerJob(syncer *Syncer) {
clearCron(syncer.Name)
}

View File

@@ -14,13 +14,25 @@
package object
import "fmt"
import (
"fmt"
"time"
)
func (syncer *Syncer) syncUsers() {
fmt.Printf("Running syncUsers()..\n")
users, userMap := syncer.getUserMap()
oUsers, oUserMap := syncer.getOriginalUserMap()
oUsers, oUserMap, err := syncer.getOriginalUserMap()
if err != nil {
fmt.Printf(err.Error())
timestamp := time.Now().Format("2006-01-02 15:04:05")
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
updateSyncerErrorText(syncer, line)
return
}
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
var affiliationMap map[int]string

View File

@@ -25,42 +25,45 @@ import (
type OriginalUser = User
func (syncer *Syncer) getOriginalUsers() []*OriginalUser {
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
sql := fmt.Sprintf("select * from %s", syncer.getTable())
results, err := syncer.Adapter.Engine.QueryString(sql)
if err != nil {
panic(err)
return nil, err
}
return syncer.getOriginalUsersFromMap(results)
return syncer.getOriginalUsersFromMap(results), nil
}
func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*OriginalUser) {
users := syncer.getOriginalUsers()
func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*OriginalUser, error) {
users, err := syncer.getOriginalUsers()
if err != nil {
return users, nil, err
}
m := map[string]*OriginalUser{}
for _, user := range users {
m[user.Id] = user
}
return users, m
return users, m, nil
}
func (syncer *Syncer) addUser(user *OriginalUser) bool {
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
keyString, valueString := syncer.getSqlKeyValueStringFromMap(m)
sql := fmt.Sprintf("insert into %s (%s) values (%s)", syncer.getTable(), keyString, valueString)
res, err := syncer.Adapter.Engine.Exec(sql)
if err != nil {
panic(err)
return false, err
}
affected, err := res.RowsAffected()
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
/*func (syncer *Syncer) getOriginalColumns() []string {
@@ -84,7 +87,7 @@ func (syncer *Syncer) getCasdoorColumns() []string {
return res
}
func (syncer *Syncer) updateUser(user *OriginalUser) bool {
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
pkValue := m[syncer.TablePrimaryKey]
delete(m, syncer.TablePrimaryKey)
@@ -93,22 +96,22 @@ func (syncer *Syncer) updateUser(user *OriginalUser) bool {
sql := fmt.Sprintf("update %s set %s where %s = %s", syncer.getTable(), setString, syncer.TablePrimaryKey, pkValue)
res, err := syncer.Adapter.Engine.Exec(sql)
if err != nil {
panic(err)
return false, err
}
affected, err := res.RowsAffected()
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
func (syncer *Syncer) updateUserForOriginalFields(user *User) bool {
func (syncer *Syncer) updateUserForOriginalFields(user *User) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(user.GetId())
oldUser := getUserById(owner, name)
if oldUser == nil {
return false
return false, nil
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
@@ -119,10 +122,10 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User) bool {
columns = append(columns, "affiliation", "hash", "pre_hash")
affected, err := adapter.Engine.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
func (syncer *Syncer) calculateHash(user *OriginalUser) string {
@@ -158,22 +161,7 @@ func (syncer *Syncer) initAdapter() {
func RunSyncUsersJob() {
syncers := GetSyncers("admin")
for _, syncer := range syncers {
if !syncer.IsEnabled {
continue
}
syncer.initAdapter()
syncer.syncUsers()
// run at every minute
//schedule := fmt.Sprintf("* * * * %d", syncer.SyncInterval)
schedule := "* * * * *"
ctab := getCrontab(syncer.Name)
err := ctab.AddJob(schedule, syncer.syncUsers)
if err != nil {
panic(err)
}
addSyncerJob(syncer)
}
time.Sleep(time.Duration(1<<63 - 1))

View File

@@ -28,7 +28,7 @@ type User struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100)" json:"id"`
Id string `xorm:"varchar(100) index" json:"id"`
Type string `xorm:"varchar(100)" json:"type"`
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
@@ -270,7 +270,7 @@ func GetLastUser(owner string) *User {
return nil
}
func UpdateUser(id string, user *User, columns []string) bool {
func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) bool {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
oldUser := getUser(owner, name)
if oldUser == nil {
@@ -285,9 +285,12 @@ func UpdateUser(id string, user *User, columns []string) bool {
if len(columns) == 0 {
columns = []string{"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"}
}
if isGlobalAdmin {
columns = append(columns, "name")
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
if err != nil {
@@ -348,9 +351,10 @@ func AddUsers(users []*User) bool {
return false
}
organization := GetOrganizationByUser(users[0])
//organization := GetOrganizationByUser(users[0])
for _, user := range users {
user.UpdateUserPassword(organization)
// this function is only used for syncer or batch upload, so no need to encrypt the password
//user.UpdateUserPassword(organization)
user.UpdateUserHash()
user.PreHash = user.Hash

View File

@@ -59,7 +59,7 @@ export const PasswordModal = (props) => {
return (
<Row>
<Button type="default" onClick={showModal}>
<Button type="default" disabled={props.disabled} onClick={showModal}>
{ hasOldPassword ? i18next.t("user:Modify password...") : i18next.t("user:Set password...")}
</Button>
<Modal

View File

@@ -112,7 +112,12 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.owner} onChange={(value => {this.updatePermissionField('owner', value);})}>
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.owner} onChange={(owner => {
this.updatePermissionField('owner', owner);
this.getUsers(owner);
this.getRoles(owner);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}

View File

@@ -21,6 +21,11 @@ import * as Setting from "./Setting";
import i18next from "i18next";
import SyncerTableColumnTable from "./SyncerTableColumnTable";
import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css');
require("codemirror/mode/javascript/javascript");
const { Option } = Select;
class SyncerEditPage extends React.Component {
@@ -248,6 +253,22 @@ class SyncerEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Error text"), i18next.t("syncer:Error text - Tooltip"))} :
</Col>
<Col span={22} >
<div style={{width: "100%", height: "300px"}} >
<CodeMirror
value={this.state.syncer.errorText}
options={{mode: 'javascript', theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateSyncerField("errorText", value);
}}
/>
</div>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :

View File

@@ -41,7 +41,7 @@ class SyncerListPage extends BaseListPage {
tableColumns: [],
affiliationTable: "",
avatarBaseUrl: "",
syncInterval: 1,
syncInterval: 10,
isEnabled: true,
}
}

View File

@@ -45,12 +45,14 @@ class UserEditPage extends React.Component {
user: null,
application: null,
organizations: [],
applications: [],
};
}
UNSAFE_componentWillMount() {
this.getUser();
this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName);
this.getUserApplication();
}
@@ -72,6 +74,15 @@ class UserEditPage extends React.Component {
});
}
getApplicationsByOrganization(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: (res.msg === undefined) ? res : [],
});
});
}
getUserApplication() {
ApplicationBackend.getUserApplication(this.state.organizationName, this.state.userName)
.then((application) => {
@@ -140,7 +151,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.name} disabled={true} onChange={e => {
<Input value={this.state.user.name} disabled={!Setting.isAdminUser(this.props.account)} onChange={e => {
this.updateUserField('name', e.target.value);
}} />
</Col>
@@ -203,7 +214,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<PasswordModal user={this.state.user} account={this.props.account} />
<PasswordModal user={this.state.user} account={this.props.account} disabled={this.state.userName !== this.state.user.name} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
@@ -293,6 +304,18 @@ class UserEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.user.signupApplication} onChange={(value => {this.updateUserField('signupApplication', value);})}>
{
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
}
</Select>
</Col>
</Row>
{
!this.isSelfOrAdmin() ? null : (
<Row style={{marginTop: '20px'}} >

View File

@@ -21,6 +21,13 @@ export function getApplications(owner, page = "", pageSize = "", field = "", val
}).then(res => res.json());
}
export function getApplicationsByOrganization(owner, organization) {
return fetch(`${Setting.ServerUrl}/api/get-applications?owner=${owner}&organization=${organization}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getApplication(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-application?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "sign in url",
"Signup URL": "Registrierungs-URL",
"Signup URL - Tooltip": "sign up url",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
"Swagger": "Swagger",
"Syncers": "Syncers",
@@ -384,6 +386,8 @@
"Database type": "Datenbanktyp",
"Database type - Tooltip": "Datenbanktyp - Tooltip",
"Edit Syncer": "Syncer bearbeiten",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Ist gehasht",
"Sync interval": "Sync-Intervall",
"Sync interval - Tooltip": "Sync-Intervall - Tooltip",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "Signin URL - Tooltip",
"Signup URL": "Signup URL",
"Signup URL - Tooltip": "Signup URL - Tooltip",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"Swagger": "Swagger",
"Syncers": "Syncers",
@@ -384,6 +386,8 @@
"Database type": "Database type",
"Database type - Tooltip": "Database type - Tooltip",
"Edit Syncer": "Edit Syncer",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "sign in url",
"Signup URL": "URL d'inscription",
"Signup URL - Tooltip": "sign up url",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"Swagger": "Swagger",
"Syncers": "Synchronisateurs",
@@ -384,6 +386,8 @@
"Database type": "Type de base de données",
"Database type - Tooltip": "Type de base de données - infobulle",
"Edit Syncer": "Editer le synchro",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Est haché",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Intervalle de synchronisation - infobulle",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "sign in url",
"Signup URL": "サインアップURL",
"Signup URL - Tooltip": "sign up url",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
"Swagger": "Swagger",
"Syncers": "Syncers",
@@ -384,6 +386,8 @@
"Database type": "データベースの種類",
"Database type - Tooltip": "データベース タイプ - ツールチップ",
"Edit Syncer": "同期ツールを編集",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "ハッシュされました",
"Sync interval": "同期間隔",
"Sync interval - Tooltip": "同期間隔 - ツールチップ",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "sign in url",
"Signup URL": "Signup URL",
"Signup URL - Tooltip": "sign up url",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"Swagger": "Swagger",
"Syncers": "Syncers",
@@ -384,6 +386,8 @@
"Database type": "Database type",
"Database type - Tooltip": "Database type - Tooltip",
"Edit Syncer": "Edit Syncer",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "sign in url",
"Signup URL": "URL регистрации",
"Signup URL - Tooltip": "sign up url",
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
"Swagger": "Swagger",
"Syncers": "Синхронизаторы",
@@ -384,6 +386,8 @@
"Database type": "Тип базы данных",
"Database type - Tooltip": "Тип базы данных - Подсказка",
"Edit Syncer": "Изменить синхронизатор",
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Хэшировано",
"Sync interval": "Интервал синхронизации",
"Sync interval - Tooltip": "Интервал синхронизации - Tooltip",

View File

@@ -154,6 +154,8 @@
"Signin URL - Tooltip": "用户的登录地址",
"Signup URL": "注册URL",
"Signup URL - Tooltip": "展示给用户的注册地址",
"Signup application": "注册应用",
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
"Swagger": "API文档",
"Syncers": "同步器",
@@ -384,9 +386,11 @@
"Database type": "数据库类型",
"Database type - Tooltip": "数据库类型",
"Edit Syncer": "编辑同步器",
"Error text": "错误信息",
"Error text - Tooltip": "同步器连接数据库时发生的错误",
"Is hashed": "是否参与哈希计算",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为分钟",
"Sync interval - Tooltip": "单位为",
"Table": "表名",
"Table - Tooltip": "数据库表名",
"Table columns": "表格列",