mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-26 09:10:22 +08:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0d5f49e40a | ||
![]() |
3527e070a0 | ||
![]() |
0108b58db4 | ||
![]() |
976b5766a5 | ||
![]() |
a92d20162a | ||
![]() |
204b1c2b8c | ||
![]() |
49fb269170 | ||
![]() |
c532a5d54d | ||
![]() |
89df80baca | ||
![]() |
d988ac814c | ||
![]() |
e4b25055d5 | ||
![]() |
4123d47174 | ||
![]() |
fbdd5a926d | ||
![]() |
92b6fda0f6 | ||
![]() |
6a7ac35e65 | ||
![]() |
fc137b9f76 | ||
![]() |
11dbd5ba9a | ||
![]() |
19942a8bd4 | ||
![]() |
f9ee8a68cb |
@@ -161,6 +161,11 @@ func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else if urlPath == "/api/upload-resource" {
|
||||
if subOwner == "app" && subName == "app-casibase" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@@ -52,6 +52,15 @@ func (c *ApiController) GetResources() {
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
isOrgAdmin, ok := c.IsOrgAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if isOrgAdmin {
|
||||
user = ""
|
||||
}
|
||||
|
||||
if sortField == "Direct" {
|
||||
provider, err := c.GetProviderFromContext("Storage")
|
||||
if err != nil {
|
||||
|
@@ -108,12 +108,12 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
|
||||
c.ResponseError(err.Error())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ClearUserSession()
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return user, true
|
||||
}
|
||||
|
||||
@@ -130,6 +130,30 @@ func (c *ApiController) RequireAdmin() (string, bool) {
|
||||
return user.Owner, true
|
||||
}
|
||||
|
||||
func (c *ApiController) IsOrgAdmin() (bool, bool) {
|
||||
userId, ok := c.RequireSignedIn()
|
||||
if !ok {
|
||||
return false, true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(userId, "app/") {
|
||||
return true, true
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return false, false
|
||||
}
|
||||
if user == nil {
|
||||
c.ClearUserSession()
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return false, false
|
||||
}
|
||||
|
||||
return user.IsAdmin, true
|
||||
}
|
||||
|
||||
// IsMaskedEnabled ...
|
||||
func (c *ApiController) IsMaskedEnabled() (bool, bool) {
|
||||
isMaskEnabled := true
|
||||
|
@@ -469,14 +469,24 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
application.FailedSigninFrozenTime = DefaultFailedSigninFrozenTime
|
||||
}
|
||||
|
||||
isOrgUser := false
|
||||
if userId != "" {
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return application
|
||||
}
|
||||
|
||||
user, _ := GetUser(userId)
|
||||
if user != nil && user.IsApplicationAdmin(application) {
|
||||
return application
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if user != nil {
|
||||
if user.IsApplicationAdmin(application) {
|
||||
return application
|
||||
}
|
||||
|
||||
if user.Owner == application.Organization {
|
||||
isOrgUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,8 +529,11 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
application.OrganizationObj.InitScore = -1
|
||||
application.OrganizationObj.EnableSoftDeletion = false
|
||||
application.OrganizationObj.IsProfilePublic = false
|
||||
application.OrganizationObj.MfaItems = nil
|
||||
application.OrganizationObj.AccountItems = nil
|
||||
|
||||
if !isOrgUser {
|
||||
application.OrganizationObj.MfaItems = nil
|
||||
application.OrganizationObj.AccountItems = nil
|
||||
}
|
||||
}
|
||||
|
||||
return application
|
||||
@@ -669,7 +682,7 @@ func (application *Application) GetId() string {
|
||||
}
|
||||
|
||||
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
|
||||
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app"}, application.RedirectUris...)
|
||||
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app", ".chromiumapp.org"}, application.RedirectUris...)
|
||||
for _, targetUri := range redirectUris {
|
||||
targetUriRegex := regexp.MustCompile(targetUri)
|
||||
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
|
||||
|
@@ -17,29 +17,35 @@ package object
|
||||
import (
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
|
||||
)
|
||||
|
||||
type InitData struct {
|
||||
Organizations []*Organization `json:"organizations"`
|
||||
Applications []*Application `json:"applications"`
|
||||
Users []*User `json:"users"`
|
||||
Certs []*Cert `json:"certs"`
|
||||
Providers []*Provider `json:"providers"`
|
||||
Ldaps []*Ldap `json:"ldaps"`
|
||||
Models []*Model `json:"models"`
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
Payments []*Payment `json:"payments"`
|
||||
Products []*Product `json:"products"`
|
||||
Resources []*Resource `json:"resources"`
|
||||
Roles []*Role `json:"roles"`
|
||||
Syncers []*Syncer `json:"syncers"`
|
||||
Tokens []*Token `json:"tokens"`
|
||||
Webhooks []*Webhook `json:"webhooks"`
|
||||
Groups []*Group `json:"groups"`
|
||||
Adapters []*Adapter `json:"adapters"`
|
||||
Enforcers []*Enforcer `json:"enforcers"`
|
||||
Plans []*Plan `json:"plans"`
|
||||
Pricings []*Pricing `json:"pricings"`
|
||||
Organizations []*Organization `json:"organizations"`
|
||||
Applications []*Application `json:"applications"`
|
||||
Users []*User `json:"users"`
|
||||
Certs []*Cert `json:"certs"`
|
||||
Providers []*Provider `json:"providers"`
|
||||
Ldaps []*Ldap `json:"ldaps"`
|
||||
Models []*Model `json:"models"`
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
Payments []*Payment `json:"payments"`
|
||||
Products []*Product `json:"products"`
|
||||
Resources []*Resource `json:"resources"`
|
||||
Roles []*Role `json:"roles"`
|
||||
Syncers []*Syncer `json:"syncers"`
|
||||
Tokens []*Token `json:"tokens"`
|
||||
Webhooks []*Webhook `json:"webhooks"`
|
||||
Groups []*Group `json:"groups"`
|
||||
Adapters []*Adapter `json:"adapters"`
|
||||
Enforcers []*Enforcer `json:"enforcers"`
|
||||
Plans []*Plan `json:"plans"`
|
||||
Pricings []*Pricing `json:"pricings"`
|
||||
Invitations []*Invitation `json:"invitations"`
|
||||
Records []*casvisorsdk.Record `json:"records"`
|
||||
Sessions []*Session `json:"sessions"`
|
||||
Subscriptions []*Subscription `json:"subscriptions"`
|
||||
Transactions []*Transaction `json:"transactions"`
|
||||
}
|
||||
|
||||
func InitFromFile() {
|
||||
@@ -114,6 +120,21 @@ func InitFromFile() {
|
||||
for _, pricing := range initData.Pricings {
|
||||
initDefinedPricing(pricing)
|
||||
}
|
||||
for _, invitation := range initData.Invitations {
|
||||
initDefinedInvitation(invitation)
|
||||
}
|
||||
for _, record := range initData.Records {
|
||||
initDefinedRecord(record)
|
||||
}
|
||||
for _, session := range initData.Sessions {
|
||||
initDefinedSession(session)
|
||||
}
|
||||
for _, subscription := range initData.Subscriptions {
|
||||
initDefinedSubscription(subscription)
|
||||
}
|
||||
for _, transaction := range initData.Transactions {
|
||||
initDefinedTransaction(transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +166,11 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
||||
Enforcers: []*Enforcer{},
|
||||
Plans: []*Plan{},
|
||||
Pricings: []*Pricing{},
|
||||
Invitations: []*Invitation{},
|
||||
Records: []*casvisorsdk.Record{},
|
||||
Sessions: []*Session{},
|
||||
Subscriptions: []*Subscription{},
|
||||
Transactions: []*Transaction{},
|
||||
}
|
||||
err := util.JsonToStruct(s, data)
|
||||
if err != nil {
|
||||
@@ -225,6 +251,11 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
||||
pricing.Plans = []string{}
|
||||
}
|
||||
}
|
||||
for _, session := range data.Sessions {
|
||||
if session.SessionId == nil {
|
||||
session.SessionId = []string{}
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -543,3 +574,61 @@ func initDefinedPricing(pricing *Pricing) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedInvitation(invitation *Invitation) {
|
||||
existed, err := getInvitation(invitation.Owner, invitation.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
return
|
||||
}
|
||||
invitation.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddInvitation(invitation, "en")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedRecord(record *casvisorsdk.Record) {
|
||||
record.CreatedTime = util.GetCurrentTime()
|
||||
_ = AddRecord(record)
|
||||
}
|
||||
|
||||
func initDefinedSession(session *Session) {
|
||||
session.CreatedTime = util.GetCurrentTime()
|
||||
_, err := AddSession(session)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedSubscription(subscription *Subscription) {
|
||||
existed, err := getSubscription(subscription.Owner, subscription.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
return
|
||||
}
|
||||
subscription.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddSubscription(subscription)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedTransaction(transaction *Transaction) {
|
||||
existed, err := getTransaction(transaction.Owner, transaction.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
return
|
||||
}
|
||||
transaction.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddTransaction(transaction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@@ -121,6 +121,31 @@ func writeInitDataToFile(filePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
invitations, err := GetInvitations("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := GetRecords()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessions, err := GetSessions("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptions("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transactions, err := GetTransactions("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := &InitData{
|
||||
Organizations: organizations,
|
||||
Applications: applications,
|
||||
@@ -142,6 +167,11 @@ func writeInitDataToFile(filePath string) error {
|
||||
Enforcers: enforcers,
|
||||
Plans: plans,
|
||||
Pricings: pricings,
|
||||
Invitations: invitations,
|
||||
Records: records,
|
||||
Sessions: sessions,
|
||||
Subscriptions: subscriptions,
|
||||
Transactions: transactions,
|
||||
}
|
||||
|
||||
text := util.StructToJsonFormatted(data)
|
||||
|
@@ -134,13 +134,19 @@ func GetRecordsByField(record *casvisorsdk.Record) ([]*casvisorsdk.Record, error
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func getFilteredWebhooks(webhooks []*Webhook, action string) []*Webhook {
|
||||
func getFilteredWebhooks(webhooks []*Webhook, organization string, action string) []*Webhook {
|
||||
res := []*Webhook{}
|
||||
for _, webhook := range webhooks {
|
||||
if !webhook.IsEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if webhook.SingleOrgOnly {
|
||||
if webhook.Organization != organization {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
matched := false
|
||||
for _, event := range webhook.Events {
|
||||
if action == event {
|
||||
@@ -163,7 +169,7 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Action)
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
for _, webhook := range webhooks {
|
||||
var user *User
|
||||
if webhook.IsUserExtended {
|
||||
|
@@ -273,7 +273,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
// base64 decode
|
||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||
if err != nil {
|
||||
return "", "", method, fmt.Errorf("err: Failed to decode SAML request , %s", err.Error())
|
||||
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
||||
}
|
||||
|
||||
// decompress
|
||||
@@ -281,7 +281,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||
|
||||
for {
|
||||
_, err := io.CopyN(&buffer, rdr, 1024)
|
||||
_, err = io.CopyN(&buffer, rdr, 1024)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
@@ -293,12 +293,12 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
var authnRequest saml.AuthNRequest
|
||||
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
|
||||
if err != nil {
|
||||
return "", "", method, fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request. %s", err.Error())
|
||||
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
|
||||
}
|
||||
|
||||
// verify samlRequest
|
||||
if isValid := application.IsRedirectUriValid(authnRequest.Issuer); !isValid {
|
||||
return "", "", method, fmt.Errorf("err: Issuer URI: %s doesn't exist in the allowed Redirect URI list", authnRequest.Issuer)
|
||||
return "", "", "", fmt.Errorf("err: Issuer URI: %s doesn't exist in the allowed Redirect URI list", authnRequest.Issuer)
|
||||
}
|
||||
|
||||
// get certificate string
|
||||
@@ -323,8 +323,13 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
}
|
||||
|
||||
_, originBackend := getOriginFromHost(host)
|
||||
|
||||
// build signedResponse
|
||||
samlResponse, _ := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer, authnRequest.ID, application.RedirectUris)
|
||||
samlResponse, err := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer, authnRequest.ID, application.RedirectUris)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: NewSamlResponse() error, %s", err.Error())
|
||||
}
|
||||
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: certificate,
|
||||
@@ -336,18 +341,23 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
|
||||
}
|
||||
|
||||
//signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
|
||||
//if err != nil {
|
||||
// signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
|
||||
// if err != nil {
|
||||
// return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
//}
|
||||
// }
|
||||
|
||||
sig, err := ctx.ConstructSignature(samlResponse, true)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: Failed to serializes the SAML request into bytes, %s", err.Error())
|
||||
}
|
||||
|
||||
samlResponse.InsertChildAt(1, sig)
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(samlResponse)
|
||||
xmlBytes, err := doc.WriteToBytes()
|
||||
if err != nil {
|
||||
return "", "", method, fmt.Errorf("err: Failed to serializes the SAML request into bytes, %s", err.Error())
|
||||
return "", "", "", fmt.Errorf("err: Failed to serializes the SAML request into bytes, %s", err.Error())
|
||||
}
|
||||
|
||||
// compress
|
||||
@@ -355,16 +365,19 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
flated := bytes.NewBuffer(nil)
|
||||
writer, err := flate.NewWriter(flated, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return "", "", method, err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, err = writer.Write(xmlBytes)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
xmlBytes = flated.Bytes()
|
||||
}
|
||||
// base64 encode
|
||||
@@ -373,12 +386,12 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
}
|
||||
|
||||
// NewSamlResponse11 return a saml1.1 response(not 2.0)
|
||||
func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
|
||||
func NewSamlResponse11(user *User, requestID string, host string) (*etree.Element, error) {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
}
|
||||
// create samlresponse
|
||||
|
||||
samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:1.0:protocol")
|
||||
samlResponse.CreateAttr("MajorVersion", "1")
|
||||
samlResponse.CreateAttr("MinorVersion", "1")
|
||||
@@ -431,11 +444,15 @@ func NewSamlResponse11(user *User, requestID string, host string) *etree.Element
|
||||
subjectConfirmationInAttribute := subjectInAttribute.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmationInAttribute.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")
|
||||
|
||||
data, _ := json.Marshal(user)
|
||||
tmp := map[string]string{}
|
||||
err := json.Unmarshal(data, &tmp)
|
||||
data, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmp := map[string]string{}
|
||||
err = json.Unmarshal(data, &tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range tmp {
|
||||
@@ -447,7 +464,7 @@ func NewSamlResponse11(user *User, requestID string, host string) *etree.Element
|
||||
}
|
||||
}
|
||||
|
||||
return samlResponse
|
||||
return samlResponse, nil
|
||||
}
|
||||
|
||||
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
|
||||
|
705
object/token.go
705
object/token.go
@@ -16,33 +16,13 @@ package object
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
const (
|
||||
hourSeconds = int(time.Hour / time.Second)
|
||||
InvalidRequest = "invalid_request"
|
||||
InvalidClient = "invalid_client"
|
||||
InvalidGrant = "invalid_grant"
|
||||
UnauthorizedClient = "unauthorized_client"
|
||||
UnsupportedGrantType = "unsupported_grant_type"
|
||||
InvalidScope = "invalid_scope"
|
||||
EndpointError = "endpoint_error"
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@@ -65,35 +45,6 @@ type Token struct {
|
||||
CodeExpireIn int64 `json:"codeExpireIn"`
|
||||
}
|
||||
|
||||
type TokenWrapper struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IdToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type TokenError struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
type IntrospectionResponse struct {
|
||||
Active bool `json:"active"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ClientId string `json:"client_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
Exp int64 `json:"exp,omitempty"`
|
||||
Iat int64 `json:"iat,omitempty"`
|
||||
Nbf int64 `json:"nbf,omitempty"`
|
||||
Sub string `json:"sub,omitempty"`
|
||||
Aud []string `json:"aud,omitempty"`
|
||||
Iss string `json:"iss,omitempty"`
|
||||
Jti string `json:"jti,omitempty"`
|
||||
}
|
||||
|
||||
func GetTokenCount(owner, organization, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Token{Organization: organization})
|
||||
@@ -279,659 +230,3 @@ func DeleteToken(token *Token) (bool, error) {
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
||||
token, err := GetTokenByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if token == nil {
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
token.ExpiresIn = 0
|
||||
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(token)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
application, err := getApplication(token.Owner, token.Application)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
return affected != 0, application, token, nil
|
||||
}
|
||||
|
||||
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
|
||||
if responseType != "code" && responseType != "token" && responseType != "id_token" {
|
||||
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil
|
||||
}
|
||||
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return i18n.Translate(lang, "token:Invalid client_id"), nil, nil
|
||||
}
|
||||
|
||||
if !application.IsRedirectUriValid(redirectUri) {
|
||||
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
|
||||
}
|
||||
|
||||
// Mask application for /api/get-app-login
|
||||
application.ClientSecret = ""
|
||||
return "", application, nil
|
||||
}
|
||||
|
||||
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return &Code{
|
||||
Message: fmt.Sprintf("general:The user: %s doesn't exist", userId),
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
if user.IsForbidden {
|
||||
return &Code{
|
||||
Message: "error: the user is forbidden to sign in, please contact the administrator",
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
msg, application, err := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state, lang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
return &Code{
|
||||
Message: msg,
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if challenge == "null" {
|
||||
challenge = ""
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeChallenge: challenge,
|
||||
CodeIsUsed: false,
|
||||
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Code{
|
||||
Message: "",
|
||||
Code: token.Code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check if grantType is allowed in the current application
|
||||
|
||||
if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" {
|
||||
return &TokenError{
|
||||
Error: UnsupportedGrantType,
|
||||
ErrorDescription: fmt.Sprintf("grant_type: %s is not supported in this application", grantType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var token *Token
|
||||
var tokenError *TokenError
|
||||
switch grantType {
|
||||
case "authorization_code": // Authorization Code Grant
|
||||
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
|
||||
case "password": // Resource Owner Password Credentials Grant
|
||||
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
|
||||
case "client_credentials": // Client Credentials Grant
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "refresh_token":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return refreshToken2, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tag == "wechat_miniprogram" {
|
||||
// Wechat Mini Program
|
||||
token, tokenError, err = GetWechatMiniProgramToken(application, code, host, username, avatar, lang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if tokenError != nil {
|
||||
return tokenError, nil
|
||||
}
|
||||
|
||||
token.CodeIsUsed = true
|
||||
|
||||
go updateUsedByCode(token)
|
||||
|
||||
tokenWrapper := &TokenWrapper{
|
||||
AccessToken: token.AccessToken,
|
||||
IdToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
TokenType: token.TokenType,
|
||||
ExpiresIn: token.ExpiresIn,
|
||||
Scope: token.Scope,
|
||||
}
|
||||
|
||||
return tokenWrapper, nil
|
||||
}
|
||||
|
||||
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
|
||||
// check parameters
|
||||
if grantType != "refresh_token" {
|
||||
return &TokenError{
|
||||
Error: UnsupportedGrantType,
|
||||
ErrorDescription: "grant_type should be refresh_token",
|
||||
}, nil
|
||||
}
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if clientSecret != "" && application.ClientSecret != clientSecret {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check whether the refresh token is valid, and has not expired.
|
||||
token, err := GetTokenByRefreshToken(refreshToken)
|
||||
if err != nil || token == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is invalid, expired or revoked",
|
||||
}, nil
|
||||
}
|
||||
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, err = ParseJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generate a new token
|
||||
user, err := getUser(application.Organization, token.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
newToken := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
_, err = AddToken(newToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = DeleteToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenWrapper := &TokenWrapper{
|
||||
AccessToken: newToken.AccessToken,
|
||||
IdToken: newToken.AccessToken,
|
||||
RefreshToken: newToken.RefreshToken,
|
||||
TokenType: newToken.TokenType,
|
||||
ExpiresIn: newToken.ExpiresIn,
|
||||
Scope: newToken.Scope,
|
||||
}
|
||||
return tokenWrapper, nil
|
||||
}
|
||||
|
||||
// PkceChallenge: base64-URL-encoded SHA256 hash of verifier, per rfc 7636
|
||||
func pkceChallenge(verifier string) string {
|
||||
sum := sha256.Sum256([]byte(verifier))
|
||||
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
|
||||
return challenge
|
||||
}
|
||||
|
||||
// IsGrantTypeValid
|
||||
// Check if grantType is allowed in the current application
|
||||
// authorization_code is allowed by default
|
||||
func IsGrantTypeValid(method string, grantTypes []string) bool {
|
||||
if method == "authorization_code" {
|
||||
return true
|
||||
}
|
||||
for _, m := range grantTypes {
|
||||
if m == method {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAuthorizationCodeToken
|
||||
// Authorization code flow
|
||||
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError, error) {
|
||||
if code == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: "authorization code should not be empty",
|
||||
}, nil
|
||||
}
|
||||
|
||||
token, err := getTokenByCode(code)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code is invalid",
|
||||
}, nil
|
||||
}
|
||||
if token.CodeIsUsed {
|
||||
// anti replay attacks
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code has been used",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "verifier is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if application.ClientSecret != clientSecret {
|
||||
// when using PKCE, the Client Secret can be empty,
|
||||
// but if it is provided, it must be accurate.
|
||||
if token.CodeChallenge == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
} else {
|
||||
if clientSecret != "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if application.Name != token.Application {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the token is for wrong application (client_id)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if time.Now().Unix() > token.CodeExpireIn {
|
||||
// code must be used within 5 minutes
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code has expired",
|
||||
}, nil
|
||||
}
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetPasswordToken
|
||||
// Resource Owner Password Credentials flow
|
||||
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
|
||||
user, err := GetUserByFields(application.Organization, username)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user does not exist",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.Ldap != "" {
|
||||
err = checkLdapUserPassword(user, password, "en")
|
||||
} else {
|
||||
err = CheckPassword(user, password, "en")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetClientCredentialsToken
|
||||
// Client Credentials flow
|
||||
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, *TokenError, error) {
|
||||
if application.ClientSecret != clientSecret {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
nullUser := &User{
|
||||
Owner: application.Owner,
|
||||
Id: application.GetId(),
|
||||
Name: application.Name,
|
||||
Type: "application",
|
||||
}
|
||||
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: application.Organization,
|
||||
User: nullUser.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetTokenByUser
|
||||
// Implicit flow
|
||||
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
|
||||
err := ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GetWechatMiniProgramToken
|
||||
// Wechat Mini Program flow
|
||||
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string, lang string) (*Token, *TokenError, error) {
|
||||
mpProvider := GetWechatMiniProgramProvider(application)
|
||||
if mpProvider == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "the application does not support wechat mini program",
|
||||
}, nil
|
||||
}
|
||||
provider, err := GetProvider(util.GetId("admin", mpProvider.Name))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
|
||||
session, err := mpIdp.GetSessionByCode(code)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("get wechat mini program session error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
openId, unionId := session.Openid, session.Unionid
|
||||
if openId == "" && unionId == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: "the wechat mini program session is invalid",
|
||||
}, nil
|
||||
}
|
||||
user, err := getUserByWechatId(application.Organization, openId, unionId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if !application.EnableSignUp {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the application does not allow to sign up new account",
|
||||
}, nil
|
||||
}
|
||||
// Add new user
|
||||
var name string
|
||||
if CheckUsername(username, lang) == "" {
|
||||
name = username
|
||||
} else {
|
||||
name = fmt.Sprintf("wechat-%s", openId)
|
||||
}
|
||||
|
||||
user = &User{
|
||||
Owner: application.Organization,
|
||||
Id: util.GenerateId(),
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
SignupApplication: application.Name,
|
||||
WeChat: openId,
|
||||
Type: "normal-user",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
IsAdmin: false,
|
||||
IsForbidden: false,
|
||||
IsDeleted: false,
|
||||
Properties: map[string]string{
|
||||
UserPropertiesWechatOpenId: openId,
|
||||
UserPropertiesWechatUnionId: unionId,
|
||||
},
|
||||
}
|
||||
_, err = AddUser(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: session.SessionKey, // a trick, because miniprogram does not use the code, so use the code field to save the session_key
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
Scope: "",
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return token, nil, nil
|
||||
}
|
||||
|
@@ -256,7 +256,7 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
|
||||
|
||||
ticket := request.AssertionArtifact.InnerXML
|
||||
if ticket == "" {
|
||||
return "", "", fmt.Errorf("samlp:AssertionArtifact field not found")
|
||||
return "", "", fmt.Errorf("request.AssertionArtifact.InnerXML error, AssertionArtifact field not found")
|
||||
}
|
||||
|
||||
ok, _, service, userId := GetCasTokenByTicket(ticket)
|
||||
@@ -282,7 +282,10 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
|
||||
return "", "", fmt.Errorf("application for user %s found", userId)
|
||||
}
|
||||
|
||||
samlResponse := NewSamlResponse11(user, request.RequestID, host)
|
||||
samlResponse, err := NewSamlResponse11(user, request.RequestID, host)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
|
728
object/token_oauth.go
Normal file
728
object/token_oauth.go
Normal file
@@ -0,0 +1,728 @@
|
||||
// Copyright 2024 The Casdoor 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 (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
const (
|
||||
hourSeconds = int(time.Hour / time.Second)
|
||||
InvalidRequest = "invalid_request"
|
||||
InvalidClient = "invalid_client"
|
||||
InvalidGrant = "invalid_grant"
|
||||
UnauthorizedClient = "unauthorized_client"
|
||||
UnsupportedGrantType = "unsupported_grant_type"
|
||||
InvalidScope = "invalid_scope"
|
||||
EndpointError = "endpoint_error"
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
}
|
||||
|
||||
type TokenWrapper struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IdToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type TokenError struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
type IntrospectionResponse struct {
|
||||
Active bool `json:"active"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ClientId string `json:"client_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
Exp int64 `json:"exp,omitempty"`
|
||||
Iat int64 `json:"iat,omitempty"`
|
||||
Nbf int64 `json:"nbf,omitempty"`
|
||||
Sub string `json:"sub,omitempty"`
|
||||
Aud []string `json:"aud,omitempty"`
|
||||
Iss string `json:"iss,omitempty"`
|
||||
Jti string `json:"jti,omitempty"`
|
||||
}
|
||||
|
||||
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
||||
token, err := GetTokenByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if token == nil {
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
token.ExpiresIn = 0
|
||||
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(token)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
application, err := getApplication(token.Owner, token.Application)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
return affected != 0, application, token, nil
|
||||
}
|
||||
|
||||
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
|
||||
if responseType != "code" && responseType != "token" && responseType != "id_token" {
|
||||
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil
|
||||
}
|
||||
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return i18n.Translate(lang, "token:Invalid client_id"), nil, nil
|
||||
}
|
||||
|
||||
if !application.IsRedirectUriValid(redirectUri) {
|
||||
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
|
||||
}
|
||||
|
||||
// Mask application for /api/get-app-login
|
||||
application.ClientSecret = ""
|
||||
return "", application, nil
|
||||
}
|
||||
|
||||
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return &Code{
|
||||
Message: fmt.Sprintf("general:The user: %s doesn't exist", userId),
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
if user.IsForbidden {
|
||||
return &Code{
|
||||
Message: "error: the user is forbidden to sign in, please contact the administrator",
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
msg, application, err := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state, lang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
return &Code{
|
||||
Message: msg,
|
||||
Code: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if challenge == "null" {
|
||||
challenge = ""
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeChallenge: challenge,
|
||||
CodeIsUsed: false,
|
||||
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Code{
|
||||
Message: "",
|
||||
Code: token.Code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check if grantType is allowed in the current application
|
||||
|
||||
if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" {
|
||||
return &TokenError{
|
||||
Error: UnsupportedGrantType,
|
||||
ErrorDescription: fmt.Sprintf("grant_type: %s is not supported in this application", grantType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var token *Token
|
||||
var tokenError *TokenError
|
||||
switch grantType {
|
||||
case "authorization_code": // Authorization Code Grant
|
||||
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
|
||||
case "password": // Resource Owner Password Credentials Grant
|
||||
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
|
||||
case "client_credentials": // Client Credentials Grant
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "refresh_token":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return refreshToken2, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tag == "wechat_miniprogram" {
|
||||
// Wechat Mini Program
|
||||
token, tokenError, err = GetWechatMiniProgramToken(application, code, host, username, avatar, lang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if tokenError != nil {
|
||||
return tokenError, nil
|
||||
}
|
||||
|
||||
token.CodeIsUsed = true
|
||||
|
||||
go updateUsedByCode(token)
|
||||
|
||||
tokenWrapper := &TokenWrapper{
|
||||
AccessToken: token.AccessToken,
|
||||
IdToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
TokenType: token.TokenType,
|
||||
ExpiresIn: token.ExpiresIn,
|
||||
Scope: token.Scope,
|
||||
}
|
||||
|
||||
return tokenWrapper, nil
|
||||
}
|
||||
|
||||
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
|
||||
// check parameters
|
||||
if grantType != "refresh_token" {
|
||||
return &TokenError{
|
||||
Error: UnsupportedGrantType,
|
||||
ErrorDescription: "grant_type should be refresh_token",
|
||||
}, nil
|
||||
}
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_id is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if clientSecret != "" && application.ClientSecret != clientSecret {
|
||||
return &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check whether the refresh token is valid, and has not expired.
|
||||
token, err := GetTokenByRefreshToken(refreshToken)
|
||||
if err != nil || token == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is invalid, expired or revoked",
|
||||
}, nil
|
||||
}
|
||||
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, err = ParseJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generate a new token
|
||||
user, err := getUser(application.Organization, token.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
newToken := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
_, err = AddToken(newToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = DeleteToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenWrapper := &TokenWrapper{
|
||||
AccessToken: newToken.AccessToken,
|
||||
IdToken: newToken.AccessToken,
|
||||
RefreshToken: newToken.RefreshToken,
|
||||
TokenType: newToken.TokenType,
|
||||
ExpiresIn: newToken.ExpiresIn,
|
||||
Scope: newToken.Scope,
|
||||
}
|
||||
return tokenWrapper, nil
|
||||
}
|
||||
|
||||
// PkceChallenge: base64-URL-encoded SHA256 hash of verifier, per rfc 7636
|
||||
func pkceChallenge(verifier string) string {
|
||||
sum := sha256.Sum256([]byte(verifier))
|
||||
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
|
||||
return challenge
|
||||
}
|
||||
|
||||
// IsGrantTypeValid
|
||||
// Check if grantType is allowed in the current application
|
||||
// authorization_code is allowed by default
|
||||
func IsGrantTypeValid(method string, grantTypes []string) bool {
|
||||
if method == "authorization_code" {
|
||||
return true
|
||||
}
|
||||
for _, m := range grantTypes {
|
||||
if m == method {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAuthorizationCodeToken
|
||||
// Authorization code flow
|
||||
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError, error) {
|
||||
if code == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: "authorization code should not be empty",
|
||||
}, nil
|
||||
}
|
||||
|
||||
token, err := getTokenByCode(code)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code is invalid",
|
||||
}, nil
|
||||
}
|
||||
if token.CodeIsUsed {
|
||||
// anti replay attacks
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code has been used",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "verifier is invalid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if application.ClientSecret != clientSecret {
|
||||
// when using PKCE, the Client Secret can be empty,
|
||||
// but if it is provided, it must be accurate.
|
||||
if token.CodeChallenge == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
} else {
|
||||
if clientSecret != "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if application.Name != token.Application {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the token is for wrong application (client_id)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if time.Now().Unix() > token.CodeExpireIn {
|
||||
// code must be used within 5 minutes
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "authorization code has expired",
|
||||
}, nil
|
||||
}
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetPasswordToken
|
||||
// Resource Owner Password Credentials flow
|
||||
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
|
||||
user, err := GetUserByFields(application.Organization, username)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user does not exist",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.Ldap != "" {
|
||||
err = checkLdapUserPassword(user, password, "en")
|
||||
} else {
|
||||
err = CheckPassword(user, password, "en")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetClientCredentialsToken
|
||||
// Client Credentials flow
|
||||
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, *TokenError, error) {
|
||||
if application.ClientSecret != clientSecret {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "client_secret is invalid",
|
||||
}, nil
|
||||
}
|
||||
nullUser := &User{
|
||||
Owner: application.Owner,
|
||||
Id: application.GetId(),
|
||||
Name: application.Name,
|
||||
Type: "application",
|
||||
}
|
||||
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: application.Organization,
|
||||
User: nullUser.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetTokenByUser
|
||||
// Implicit flow
|
||||
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
|
||||
err := ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GetWechatMiniProgramToken
|
||||
// Wechat Mini Program flow
|
||||
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string, lang string) (*Token, *TokenError, error) {
|
||||
mpProvider := GetWechatMiniProgramProvider(application)
|
||||
if mpProvider == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidClient,
|
||||
ErrorDescription: "the application does not support wechat mini program",
|
||||
}, nil
|
||||
}
|
||||
provider, err := GetProvider(util.GetId("admin", mpProvider.Name))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
|
||||
session, err := mpIdp.GetSessionByCode(code)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("get wechat mini program session error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
openId, unionId := session.Openid, session.Unionid
|
||||
if openId == "" && unionId == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
ErrorDescription: "the wechat mini program session is invalid",
|
||||
}, nil
|
||||
}
|
||||
user, err := getUserByWechatId(application.Organization, openId, unionId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if !application.EnableSignUp {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the application does not allow to sign up new account",
|
||||
}, nil
|
||||
}
|
||||
// Add new user
|
||||
var name string
|
||||
if CheckUsername(username, lang) == "" {
|
||||
name = username
|
||||
} else {
|
||||
name = fmt.Sprintf("wechat-%s", openId)
|
||||
}
|
||||
|
||||
user = &User{
|
||||
Owner: application.Organization,
|
||||
Id: util.GenerateId(),
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
SignupApplication: application.Name,
|
||||
WeChat: openId,
|
||||
Type: "normal-user",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
IsAdmin: false,
|
||||
IsForbidden: false,
|
||||
IsDeleted: false,
|
||||
Properties: map[string]string{
|
||||
UserPropertiesWechatOpenId: openId,
|
||||
UserPropertiesWechatUnionId: unionId,
|
||||
},
|
||||
}
|
||||
_, err = AddUser(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: tokenName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: session.SessionKey, // a trick, because miniprogram does not use the code, so use the code field to save the session_key
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
Scope: "",
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
_, err = AddToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return token, nil, nil
|
||||
}
|
@@ -39,6 +39,7 @@ type Webhook struct {
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||
IsUserExtended bool `json:"isUserExtended"`
|
||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,6 @@ package routers
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@@ -36,7 +35,7 @@ type Response struct {
|
||||
}
|
||||
|
||||
func responseError(ctx *context.Context, error string, data ...interface{}) {
|
||||
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||
// ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||
|
||||
resp := Response{Status: "error", Msg: error}
|
||||
switch len(data) {
|
||||
|
@@ -48,7 +48,7 @@ func CorsFilter(ctx *context.Context) {
|
||||
originHostname := getHostname(origin)
|
||||
host := removePort(ctx.Request.Host)
|
||||
|
||||
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") {
|
||||
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") {
|
||||
setCorsHeaders(ctx, origin)
|
||||
return
|
||||
}
|
||||
|
@@ -366,7 +366,7 @@ class App extends Component {
|
||||
<FloatButton.BackTop />
|
||||
<CustomGithubCorner />
|
||||
{
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<Suspense fallback={null}>
|
||||
<Layout id="parent-area">
|
||||
<ManagementPage
|
||||
account={this.state.account}
|
||||
|
@@ -124,7 +124,7 @@ class ResourceListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("application"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/applications/${record.organization}/${text}`}>
|
||||
<Link to={`/applications/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
@@ -707,6 +707,15 @@ export function goToLinkSoft(ths, link) {
|
||||
ths.props.history.push(link);
|
||||
}
|
||||
|
||||
export function goToLinkSoftOrJumpSelf(ths, link) {
|
||||
if (link.startsWith("http")) {
|
||||
goToLink(link);
|
||||
return;
|
||||
}
|
||||
|
||||
ths.props.history.push(link);
|
||||
}
|
||||
|
||||
export function showMessage(type, text) {
|
||||
if (type === "success") {
|
||||
message.success(text);
|
||||
|
@@ -64,7 +64,9 @@ class UserEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getUser();
|
||||
this.getOrganizations();
|
||||
if (Setting.isLocalAdminUser(this.props.account)) {
|
||||
this.getOrganizations();
|
||||
}
|
||||
this.getApplicationsByOrganization(this.state.organizationName);
|
||||
this.getUserApplication();
|
||||
this.setReturnUrl();
|
||||
@@ -249,22 +251,8 @@ class UserEditPage extends React.Component {
|
||||
};
|
||||
|
||||
renderAccountItem(accountItem) {
|
||||
if (!accountItem.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
|
||||
if (accountItem.viewRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
return null;
|
||||
}
|
||||
} else if (accountItem.viewRule === "Admin") {
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let disabled = false;
|
||||
if (accountItem.modifyRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
@@ -1001,7 +989,7 @@ class UserEditPage extends React.Component {
|
||||
<div style={{verticalAlign: "middle", marginBottom: 10}}>{`(${i18next.t("general:empty")})`}</div>
|
||||
</Col>
|
||||
}
|
||||
<CropperDivModal disabled={disabled} tag={tag} setTitle={set} buttonText={`${title}...`} title={title} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
|
||||
<CropperDivModal disabled={disabled} tag={tag} setTitle={set} buttonText={`${title}...`} title={title} user={this.state.user} organization={this.getUserOrganization()} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -1011,7 +999,7 @@ class UserEditPage extends React.Component {
|
||||
<Card size="small" title={
|
||||
(this.props.account === null) ? i18next.t("user:User Profile") : (
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}
|
||||
{this.state.mode === "add" ? i18next.t("user:New User") : (this.isSelf() ? i18next.t("account:My Account") : i18next.t("user:Edit User"))}
|
||||
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
@@ -1021,6 +1009,21 @@ class UserEditPage extends React.Component {
|
||||
<Form>
|
||||
{
|
||||
this.getUserOrganization()?.accountItems?.map(accountItem => {
|
||||
if (!accountItem.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
|
||||
if (accountItem.viewRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
return null;
|
||||
}
|
||||
} else if (accountItem.viewRule === "Admin") {
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={accountItem.name}>
|
||||
<Form.Item name={accountItem.name}
|
||||
@@ -1091,7 +1094,9 @@ class UserEditPage extends React.Component {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
|
||||
if (location.pathname !== "/account") {
|
||||
this.props.history.push(`/users/${this.state.user.owner}/${this.state.user.name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (exitAfterSave) {
|
||||
|
@@ -309,6 +309,16 @@ class WebhookEditPage extends React.Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Single org only"), i18next.t("webhook:Single org only - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.webhook.singleOrgOnly} onChange={checked => {
|
||||
this.updateWebhookField("singleOrgOnly", checked);
|
||||
}} />
|
||||
</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"))} :
|
||||
|
@@ -111,7 +111,7 @@ class WebhookListPage extends BaseListPage {
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "180px",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
@@ -121,7 +121,7 @@ class WebhookListPage extends BaseListPage {
|
||||
title: i18next.t("general:URL"),
|
||||
dataIndex: "url",
|
||||
key: "url",
|
||||
width: "300px",
|
||||
width: "200px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("url"),
|
||||
render: (text, record, index) => {
|
||||
@@ -138,7 +138,7 @@ class WebhookListPage extends BaseListPage {
|
||||
title: i18next.t("general:Method"),
|
||||
dataIndex: "method",
|
||||
key: "method",
|
||||
width: "120px",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("method"),
|
||||
},
|
||||
@@ -146,7 +146,7 @@ class WebhookListPage extends BaseListPage {
|
||||
title: i18next.t("webhook:Content type"),
|
||||
dataIndex: "contentType",
|
||||
key: "contentType",
|
||||
width: "200px",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
filterMultiple: false,
|
||||
filters: [
|
||||
@@ -169,7 +169,19 @@ class WebhookListPage extends BaseListPage {
|
||||
title: i18next.t("webhook:Is user extended"),
|
||||
dataIndex: "isUserExtended",
|
||||
key: "isUserExtended",
|
||||
width: "160px",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("webhook:Single org only"),
|
||||
dataIndex: "singleOrgOnly",
|
||||
key: "singleOrgOnly",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
@@ -183,6 +195,7 @@ class WebhookListPage extends BaseListPage {
|
||||
key: "isEnabled",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
|
@@ -172,7 +172,7 @@ class AuthCallback extends React.Component {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
||||
} else if (responseType === "link") {
|
||||
const from = innerParams.get("from");
|
||||
Setting.goToLinkSoft(this, from);
|
||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
|
@@ -42,9 +42,7 @@ import OktaLoginButton from "./OktaLoginButton";
|
||||
import DouyinLoginButton from "./DouyinLoginButton";
|
||||
import LoginButton from "./LoginButton";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import {getEvent} from "./Util";
|
||||
import {Modal} from "antd";
|
||||
import {WechatOfficialAccountModal} from "./Util";
|
||||
|
||||
function getSigninButton(provider) {
|
||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
|
||||
@@ -141,32 +139,11 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
||||
if (size === "small") {
|
||||
if (provider.category === "OAuth") {
|
||||
if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) {
|
||||
const info = async() => {
|
||||
AuthBackend.getWechatQRCode(`${provider.owner}/${provider.name}`).then(
|
||||
async res => {
|
||||
if (res.status !== "ok") {
|
||||
Setting.showMessage("error", res?.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const t1 = setInterval(await getEvent, 1000, application, provider, res.data2);
|
||||
{Modal.info({
|
||||
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
|
||||
content: (
|
||||
<div style={{marginRight: "34px"}}>
|
||||
<img src = {"data:image/png;base64," + res.data} alt="Wechat QR code" style={{width: "100%"}} />
|
||||
</div>
|
||||
),
|
||||
onOk() {
|
||||
window.clearInterval(t1);
|
||||
},
|
||||
});}
|
||||
}
|
||||
);
|
||||
};
|
||||
return (
|
||||
<a key={provider.displayName} >
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} onClick={info} />
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} onClick={() => {
|
||||
WechatOfficialAccountModal(application, provider, "signup");
|
||||
}} />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
|
@@ -13,11 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Alert, Button, Result} from "antd";
|
||||
import {Alert, Button, Modal, Result} from "antd";
|
||||
import i18next from "i18next";
|
||||
import {getWechatMessageEvent} from "./AuthBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Provider from "./Provider";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
|
||||
export function renderMessage(msg) {
|
||||
if (msg !== null) {
|
||||
@@ -188,12 +189,36 @@ export function getQueryParamsFromState(state) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getEvent(application, provider, ticket) {
|
||||
export function getEvent(application, provider, ticket, method) {
|
||||
getWechatMessageEvent(ticket)
|
||||
.then(res => {
|
||||
if (res.data === "SCAN" || res.data === "subscribe") {
|
||||
const code = res?.data2;
|
||||
Setting.goToLink(Provider.getAuthUrl(application, provider, "signup", code));
|
||||
Setting.goToLink(Provider.getAuthUrl(application, provider, method ?? "signup", code));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function WechatOfficialAccountModal(application, provider, method) {
|
||||
AuthBackend.getWechatQRCode(`${provider.owner}/${provider.name}`).then(
|
||||
async res => {
|
||||
if (res.status !== "ok") {
|
||||
Setting.showMessage("error", res?.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const t1 = setInterval(await getEvent, 1000, application, provider, res.data2, method);
|
||||
{Modal.info({
|
||||
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
|
||||
content: (
|
||||
<div style={{marginRight: "34px"}}>
|
||||
<img src = {"data:image/png;base64," + res.data} alt="Wechat QR code" style={{width: "100%"}} />
|
||||
</div>
|
||||
),
|
||||
onOk() {
|
||||
window.clearInterval(t1);
|
||||
},
|
||||
});}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -14,39 +14,36 @@
|
||||
|
||||
import {useEffect} from "react";
|
||||
|
||||
let customHeadLoaded = false;
|
||||
|
||||
function CustomHead(props) {
|
||||
useEffect(() => {
|
||||
const suffix = new Date().getTime().toString();
|
||||
if (!customHeadLoaded) {
|
||||
const suffix = new Date().getTime().toString();
|
||||
|
||||
if (!props.headerHtml) {return;}
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = props.headerHtml;
|
||||
if (!props.headerHtml) {return;}
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = props.headerHtml;
|
||||
|
||||
node.childNodes.forEach(el => {
|
||||
if (el.nodeName === "#text") {
|
||||
return;
|
||||
}
|
||||
let innerNode = el;
|
||||
innerNode.setAttribute("app-custom-head" + suffix, "");
|
||||
|
||||
if (innerNode.localName === "script") {
|
||||
const scriptNode = document.createElement("script");
|
||||
Array.from(innerNode.attributes).forEach(attr => {
|
||||
scriptNode.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
scriptNode.text = innerNode.textContent;
|
||||
innerNode = scriptNode;
|
||||
}
|
||||
document.head.appendChild(innerNode);
|
||||
});
|
||||
|
||||
return () => {
|
||||
for (const el of document.head.children) {
|
||||
if (el.getAttribute("app-custom-head" + suffix) !== null) {
|
||||
document.head.removeChild(el);
|
||||
node.childNodes.forEach(el => {
|
||||
if (el.nodeName === "#text") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let innerNode = el;
|
||||
innerNode.setAttribute("app-custom-head" + suffix, "");
|
||||
|
||||
if (innerNode.localName === "script") {
|
||||
const scriptNode = document.createElement("script");
|
||||
Array.from(innerNode.attributes).forEach(attr => {
|
||||
scriptNode.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
scriptNode.text = innerNode.textContent;
|
||||
innerNode = scriptNode;
|
||||
}
|
||||
document.head.appendChild(innerNode);
|
||||
});
|
||||
customHeadLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -21,6 +21,7 @@ import * as Provider from "../auth/Provider";
|
||||
import * as AuthBackend from "../auth/AuthBackend";
|
||||
import {goToWeb3Url} from "../auth/ProviderButton";
|
||||
import AccountAvatar from "../account/AccountAvatar";
|
||||
import {WechatOfficialAccountModal} from "../auth/Util";
|
||||
|
||||
class OAuthWidget extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -197,9 +198,19 @@ class OAuthWidget extends React.Component {
|
||||
provider.category === "Web3" ? (
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={() => goToWeb3Url(application, provider, "link")}>{i18next.t("user:Link")}</Button>
|
||||
) : (
|
||||
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger") ? (
|
||||
<a key={provider.displayName}>
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={
|
||||
() => {
|
||||
WechatOfficialAccountModal(application, provider, "link");
|
||||
}
|
||||
}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
) : (
|
||||
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<Button disabled={!providerItem.canUnlink && !Setting.isAdminUser(account)} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type, linkedValue)}>{i18next.t("user:Unlink")}</Button>
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "URL der Prompt-Seite kopieren",
|
||||
"Copy signin page URL": "Kopieren Sie die URL der Anmeldeseite",
|
||||
"Copy signup page URL": "URL der Anmeldeseite kopieren",
|
||||
"Custom CSS": "Formular CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Bearbeiten",
|
||||
"Custom CSS - Tooltip": "CSS-Styling der Anmelde-, Registrierungs- und Passwort-vergessen-Seite (z. B. Hinzufügen von Rahmen und Schatten)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Bearbeitungsanwendung",
|
||||
"Enable Email linking": "E-Mail-Verknüpfung aktivieren",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Datei erfolgreich hochgeladen",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Folge dem Theme der Organisation",
|
||||
"Custom CSS": "Formular CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Bearbeiten",
|
||||
"Custom CSS - Tooltip": "CSS-Styling der Anmelde-, Registrierungs- und Passwort-vergessen-Seite (z. B. Hinzufügen von Rahmen und Schatten)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Formposition",
|
||||
"Form position - Tooltip": "Position der Anmelde-, Registrierungs- und Passwort-vergessen-Formulare",
|
||||
"Grant types": "Grant-Typen",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Sollten die erweiterten Felder des Benutzers in das JSON inkludiert werden?",
|
||||
"Method - Tooltip": "HTTP Methode",
|
||||
"New Webhook": "Neue Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Wert"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copiar URL de la página del prompt",
|
||||
"Copy signin page URL": "Copiar la URL de la página de inicio de sesión",
|
||||
"Copy signup page URL": "Copiar URL de la página de registro",
|
||||
"Custom CSS": "Formulario CSS",
|
||||
"Custom CSS - Edit": "Formulario CSS - Editar",
|
||||
"Custom CSS - Tooltip": "Estilo CSS de los formularios de registro, inicio de sesión y olvido de contraseña (por ejemplo, agregar bordes y sombras)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Editar solicitud",
|
||||
"Enable Email linking": "Habilitar enlace de correo electrónico",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Archivo subido exitosamente",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Seguir el tema de la organización",
|
||||
"Custom CSS": "Formulario CSS",
|
||||
"Custom CSS - Edit": "Formulario CSS - Editar",
|
||||
"Custom CSS - Tooltip": "Estilo CSS de los formularios de registro, inicio de sesión y olvido de contraseña (por ejemplo, agregar bordes y sombras)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Posición de la Forma",
|
||||
"Form position - Tooltip": "Ubicación de los formularios de registro, inicio de sesión y olvido de contraseña",
|
||||
"Grant types": "Tipos de subvenciones",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "¿Incluir los campos extendidos del usuario en el JSON?",
|
||||
"Method - Tooltip": "Método HTTP",
|
||||
"New Webhook": "Nuevo Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Valor"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copier l'URL de la page de l'invite",
|
||||
"Copy signin page URL": "Copier l'URL de la page de connexion",
|
||||
"Copy signup page URL": "Copiez l'URL de la page d'inscription",
|
||||
"Custom CSS": "CSS du formulaire",
|
||||
"Custom CSS - Edit": "CSS du formulaire - Modifier",
|
||||
"Custom CSS - Tooltip": "Mise en forme CSS des formulaires d'inscription, de connexion et de récupération de mot de passe (par exemple, en ajoutant des bordures et des ombres)",
|
||||
"Custom CSS Mobile": "CSS du formulaire sur téléphone",
|
||||
"Custom CSS Mobile - Edit": "CSS du formulaire sur téléphone - Modifier",
|
||||
"Custom CSS Mobile - Tooltip": "CSS du formulaire sur téléphone - Info-bulle",
|
||||
"Dynamic": "Dynamique",
|
||||
"Edit Application": "Modifier l'application",
|
||||
"Enable Email linking": "Autoriser à lier l'e-mail",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Fichier téléchargé avec succès",
|
||||
"First, last": "Prénom, nom",
|
||||
"Follow organization theme": "Suivre le thème de l'organisation",
|
||||
"Custom CSS": "CSS du formulaire",
|
||||
"Custom CSS - Edit": "CSS du formulaire - Modifier",
|
||||
"Custom CSS - Tooltip": "Mise en forme CSS des formulaires d'inscription, de connexion et de récupération de mot de passe (par exemple, en ajoutant des bordures et des ombres)",
|
||||
"Custom CSS Mobile": "CSS du formulaire sur téléphone",
|
||||
"Custom CSS Mobile - Edit": "CSS du formulaire sur téléphone - Modifier",
|
||||
"Custom CSS Mobile - Tooltip": "CSS du formulaire sur téléphone - Info-bulle",
|
||||
"Form position": "Position du formulaire",
|
||||
"Form position - Tooltip": "Emplacement des formulaires d'inscription, de connexion et de récupération de mot de passe",
|
||||
"Grant types": "Types d'autorisation",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Inclure les champs étendus du compte dans l'objet JSON",
|
||||
"Method - Tooltip": "Méthode HTTP",
|
||||
"New Webhook": "Nouveau webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Valeur"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Salin URL halaman prompt",
|
||||
"Copy signin page URL": "Salin URL halaman masuk",
|
||||
"Copy signup page URL": "Salin URL halaman pendaftaran",
|
||||
"Custom CSS": "Formulir CSS",
|
||||
"Custom CSS - Edit": "Formulir CSS - Edit",
|
||||
"Custom CSS - Tooltip": "Pengaturan CSS dari formulir pendaftaran, masuk, dan lupa kata sandi (misalnya menambahkan batas dan bayangan)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Mengedit aplikasi",
|
||||
"Enable Email linking": "Aktifkan pengaitan email",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Berkas telah diunggah dengan sukses",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Ikuti tema organisasi",
|
||||
"Custom CSS": "Formulir CSS",
|
||||
"Custom CSS - Edit": "Formulir CSS - Edit",
|
||||
"Custom CSS - Tooltip": "Pengaturan CSS dari formulir pendaftaran, masuk, dan lupa kata sandi (misalnya menambahkan batas dan bayangan)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Posisi formulir",
|
||||
"Form position - Tooltip": "Tempat pendaftaran, masuk, dan lupa kata sandi",
|
||||
"Grant types": "Jenis-jenis hibah",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Apakah akan menyertakan bidang-bidang tambahan pengguna dalam JSON?",
|
||||
"Method - Tooltip": "Metode HTTP",
|
||||
"New Webhook": "Webhook Baru",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Nilai"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "プロンプトページのURLをコピーしてください",
|
||||
"Copy signin page URL": "サインインページのURLをコピーしてください",
|
||||
"Copy signup page URL": "サインアップページのURLをコピーしてください",
|
||||
"Custom CSS": "フォームCSS",
|
||||
"Custom CSS - Edit": "フォームのCSS - 編集",
|
||||
"Custom CSS - Tooltip": "サインアップ、サインイン、パスワード忘れのフォームのCSSスタイリング(例:境界線や影の追加)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "アプリケーションを編集する",
|
||||
"Enable Email linking": "イーメールリンクの有効化",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "ファイルが正常にアップロードされました",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "組織のテーマに従ってください",
|
||||
"Custom CSS": "フォームCSS",
|
||||
"Custom CSS - Edit": "フォームのCSS - 編集",
|
||||
"Custom CSS - Tooltip": "サインアップ、サインイン、パスワード忘れのフォームのCSSスタイリング(例:境界線や影の追加)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "フォームのポジション",
|
||||
"Form position - Tooltip": "登録、ログイン、パスワード忘れフォームの位置",
|
||||
"Grant types": "グラント種類",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "ユーザーの拡張フィールドをJSONに含めるかどうか",
|
||||
"Method - Tooltip": "HTTPメソッド",
|
||||
"New Webhook": "新しいWebhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "値"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "프롬프트 페이지 URL을 복사하세요",
|
||||
"Copy signin page URL": "사인인 페이지 URL 복사",
|
||||
"Copy signup page URL": "가입 페이지 URL을 복사하세요",
|
||||
"Custom CSS": "CSS 양식",
|
||||
"Custom CSS - Edit": "폼 CSS - 편집",
|
||||
"Custom CSS - Tooltip": "가입, 로그인 및 비밀번호를 잊어버린 양식의 CSS 스타일링 (예 : 테두리와 그림자 추가)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "앱 편집하기",
|
||||
"Enable Email linking": "이메일 링크 사용 가능하도록 설정하기",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "파일이 성공적으로 업로드되었습니다",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "조직의 주제를 따르세요",
|
||||
"Custom CSS": "CSS 양식",
|
||||
"Custom CSS - Edit": "폼 CSS - 편집",
|
||||
"Custom CSS - Tooltip": "가입, 로그인 및 비밀번호를 잊어버린 양식의 CSS 스타일링 (예 : 테두리와 그림자 추가)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "양식 위치",
|
||||
"Form position - Tooltip": "가입, 로그인 및 비밀번호 재설정 양식의 위치",
|
||||
"Grant types": "Grant types: 부여 유형",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "사용자의 확장 필드를 JSON에 포함할지 여부",
|
||||
"Method - Tooltip": "HTTP 방법",
|
||||
"New Webhook": "새로운 웹훅",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "가치"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copiar URL da página de prompt",
|
||||
"Copy signin page URL": "Copiar URL da página de login",
|
||||
"Copy signup page URL": "Copiar URL da página de registro",
|
||||
"Custom CSS": "CSS do formulário",
|
||||
"Custom CSS - Edit": "Editar CSS do formulário",
|
||||
"Custom CSS - Tooltip": "Estilização CSS dos formulários de registro, login e recuperação de senha (por exemplo, adicionando bordas e sombras)",
|
||||
"Custom CSS Mobile": "CSS do formulário em dispositivos móveis",
|
||||
"Custom CSS Mobile - Edit": "Editar CSS do formulário em dispositivos móveis",
|
||||
"Custom CSS Mobile - Tooltip": "CSS do formulário em dispositivos móveis - Dica",
|
||||
"Dynamic": "Dinâmico",
|
||||
"Edit Application": "Editar Aplicação",
|
||||
"Enable Email linking": "Ativar vinculação de e-mail",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Arquivo enviado com sucesso",
|
||||
"First, last": "Primeiro, último",
|
||||
"Follow organization theme": "Seguir tema da organização",
|
||||
"Custom CSS": "CSS do formulário",
|
||||
"Custom CSS - Edit": "Editar CSS do formulário",
|
||||
"Custom CSS - Tooltip": "Estilização CSS dos formulários de registro, login e recuperação de senha (por exemplo, adicionando bordas e sombras)",
|
||||
"Custom CSS Mobile": "CSS do formulário em dispositivos móveis",
|
||||
"Custom CSS Mobile - Edit": "Editar CSS do formulário em dispositivos móveis",
|
||||
"Custom CSS Mobile - Tooltip": "CSS do formulário em dispositivos móveis - Dica",
|
||||
"Form position": "Posição do formulário",
|
||||
"Form position - Tooltip": "Localização dos formulários de registro, login e recuperação de senha",
|
||||
"Grant types": "Tipos de concessão",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Se incluir os campos estendidos do usuário no JSON",
|
||||
"Method - Tooltip": "Método HTTP",
|
||||
"New Webhook": "Novo Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Valor"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Скопируйте URL страницы предложения",
|
||||
"Copy signin page URL": "Скопируйте URL-адрес страницы входа",
|
||||
"Copy signup page URL": "Скопируйте URL страницы регистрации",
|
||||
"Custom CSS": "Форма CSS",
|
||||
"Custom CSS - Edit": "Форма CSS - Редактирование",
|
||||
"Custom CSS - Tooltip": "CSS-оформление форм регистрации, входа и восстановления пароля (например, добавление границ и теней)",
|
||||
"Custom CSS Mobile": "CSS формы для мобильных",
|
||||
"Custom CSS Mobile - Edit": "CSS формы для мобильный - редактировать",
|
||||
"Custom CSS Mobile - Tooltip": "Редактирование CSS кода для мобильных устройств",
|
||||
"Dynamic": "Динамическое",
|
||||
"Edit Application": "Изменить приложение",
|
||||
"Enable Email linking": "Включить связывание электронной почты",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Файл успешно загружен",
|
||||
"First, last": "Имя, Фамилия",
|
||||
"Follow organization theme": "Cледуйте теме организации",
|
||||
"Custom CSS": "Форма CSS",
|
||||
"Custom CSS - Edit": "Форма CSS - Редактирование",
|
||||
"Custom CSS - Tooltip": "CSS-оформление форм регистрации, входа и восстановления пароля (например, добавление границ и теней)",
|
||||
"Custom CSS Mobile": "CSS формы для мобильных",
|
||||
"Custom CSS Mobile - Edit": "CSS формы для мобильный - редактировать",
|
||||
"Custom CSS Mobile - Tooltip": "Редактирование CSS кода для мобильных устройств",
|
||||
"Form position": "Позиция формы",
|
||||
"Form position - Tooltip": "Местоположение форм регистрации, входа и восстановления пароля",
|
||||
"Grant types": "Типы грантов",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Нужно ли включать расширенные поля пользователя в формате JSON?",
|
||||
"Method - Tooltip": "Метод HTTP",
|
||||
"New Webhook": "Новый вебхук",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Значение"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Prompt Page URL 'ini kopyala",
|
||||
"Copy signin page URL": "Giriş sayfası URL 'ini kopyala",
|
||||
"Copy signup page URL": "Kayıt sayfası URL 'ini kopyala",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dinamik",
|
||||
"Edit Application": "Uygulamayı düzenle",
|
||||
"Enable Email linking": "Eposta bağlantısı aktif",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "Adı, Soyadı",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Copy prompt page URL",
|
||||
"Copy signin page URL": "Copy signin page URL",
|
||||
"Copy signup page URL": "Copy signup page URL",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable Email linking": "Enable Email linking",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "File uploaded successfully",
|
||||
"First, last": "First, last",
|
||||
"Follow organization theme": "Follow organization theme",
|
||||
"Custom CSS": "Custom CSS",
|
||||
"Custom CSS - Edit": "Custom CSS - Edit",
|
||||
"Custom CSS - Tooltip": "CSS styling of the signup, signin and forget password forms (e.g. adding borders and shadows)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Grant types": "Grant types",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Whether to include the user's extended fields in the JSON",
|
||||
"Method - Tooltip": "HTTP method",
|
||||
"New Webhook": "New Webhook",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "Sao chép URL của trang nhắc nhở",
|
||||
"Copy signin page URL": "Sao chép URL trang đăng nhập",
|
||||
"Copy signup page URL": "Sao chép URL trang đăng ký",
|
||||
"Custom CSS": "Mẫu CSS",
|
||||
"Custom CSS - Edit": "Biểu mẫu CSS - Sửa",
|
||||
"Custom CSS - Tooltip": "Phong cách CSS của các biểu mẫu đăng ký, đăng nhập và quên mật khẩu (ví dụ: thêm đường viền và bóng)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Dynamic": "Dynamic",
|
||||
"Edit Application": "Sửa ứng dụng",
|
||||
"Enable Email linking": "Cho phép liên kết Email",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "Tệp được tải lên thành công",
|
||||
"First, last": "Tên, Họ",
|
||||
"Follow organization theme": "Theo giao diện tổ chức",
|
||||
"Custom CSS": "Mẫu CSS",
|
||||
"Custom CSS - Edit": "Biểu mẫu CSS - Sửa",
|
||||
"Custom CSS - Tooltip": "Phong cách CSS của các biểu mẫu đăng ký, đăng nhập và quên mật khẩu (ví dụ: thêm đường viền và bóng)",
|
||||
"Custom CSS Mobile": "Custom CSS Mobile",
|
||||
"Custom CSS Mobile - Edit": "Custom CSS Mobile - Edit",
|
||||
"Custom CSS Mobile - Tooltip": "Custom CSS Mobile - Tooltip",
|
||||
"Form position": "Vị trí của hình thức",
|
||||
"Form position - Tooltip": "Vị trí của các biểu mẫu đăng ký, đăng nhập và quên mật khẩu",
|
||||
"Grant types": "Loại hỗ trợ",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "Có nên bao gồm các trường mở rộng của người dùng trong định dạng JSON không?",
|
||||
"Method - Tooltip": "Phương thức HTTP",
|
||||
"New Webhook": "Webhook mới",
|
||||
"Single org only": "Single org only",
|
||||
"Single org only - Tooltip": "Triggered only in the organization that the webhook belongs to",
|
||||
"Value": "Giá trị"
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,12 @@
|
||||
"Copy prompt page URL": "复制提醒页面URL",
|
||||
"Copy signin page URL": "复制登录页面URL",
|
||||
"Copy signup page URL": "复制注册页面URL",
|
||||
"Custom CSS": "表单CSS",
|
||||
"Custom CSS - Edit": "编辑表单CSS",
|
||||
"Custom CSS - Tooltip": "注册、登录、忘记密码等表单的CSS样式(如增加边框和阴影)",
|
||||
"Custom CSS Mobile": "表单CSS(移动端)",
|
||||
"Custom CSS Mobile - Edit": "编辑表单CSS(移动端)",
|
||||
"Custom CSS Mobile - Tooltip": "注册、登录、忘记密码等表单的CSS样式(如增加边框和阴影)(移动端)",
|
||||
"Dynamic": "动态开启",
|
||||
"Edit Application": "编辑应用",
|
||||
"Enable Email linking": "自动关联邮箱相同的账号",
|
||||
@@ -52,12 +58,6 @@
|
||||
"File uploaded successfully": "文件上传成功",
|
||||
"First, last": "名字, 姓氏",
|
||||
"Follow organization theme": "使用组织主题",
|
||||
"Custom CSS": "表单CSS",
|
||||
"Custom CSS - Edit": "编辑表单CSS",
|
||||
"Custom CSS - Tooltip": "注册、登录、忘记密码等表单的CSS样式(如增加边框和阴影)",
|
||||
"Custom CSS Mobile": "表单CSS(移动端)",
|
||||
"Custom CSS Mobile - Edit": "编辑表单CSS(移动端)",
|
||||
"Custom CSS Mobile - Tooltip": "注册、登录、忘记密码等表单的CSS样式(如增加边框和阴影)(移动端)",
|
||||
"Form position": "表单位置",
|
||||
"Form position - Tooltip": "注册、登录、忘记密码等表单的位置",
|
||||
"Grant types": "OAuth授权类型",
|
||||
@@ -1150,6 +1150,8 @@
|
||||
"Is user extended - Tooltip": "是否在JSON里加入用户的扩展字段",
|
||||
"Method - Tooltip": "HTTP方法",
|
||||
"New Webhook": "添加Webhook",
|
||||
"Single org only": "仅本组织",
|
||||
"Single org only - Tooltip": "仅在Webhook所在组织触发",
|
||||
"Value": "值"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user