Compare commits

...

20 Commits

Author SHA1 Message Date
4e17dae2c2 feat: fix unable to remove user from group bug (#3847) 2025-05-28 22:29:40 +08:00
0ad4d82d9c feat: fix GetGroups() API bug when parentGroup is in next page (#3843) 2025-05-28 18:31:52 +08:00
731daf5204 feat: allow org admin to change org user's password without old password (#3841) 2025-05-28 01:23:44 +08:00
b6b77da7cf feat: refactor the code in NewSmtpEmailProvider() (#3832) 2025-05-26 20:23:47 +08:00
8b4637aa3a feat: provide a more complete Excel template for uploading users and fix any bugs (#3831) 2025-05-25 21:23:48 +08:00
87506b84e3 feat: support special chars like "+" in username parameter of /api/get-email-and-phone API (#3824) 2025-05-23 17:29:00 +08:00
fed9332246 feat: can configure Domain field in Nextcloud OAuth provider (#3813) 2025-05-23 17:23:34 +08:00
33afc52a0b feat: can redirect user to login page after linking provider in prompt page (#3820) 2025-05-23 07:15:53 +08:00
9035ca365a feat: improve Indonesia i18n translations (#3817) 2025-05-22 20:42:47 +08:00
b97ae72179 feat: use the standard user struct for JWT-Standard to get a correct userinfo (#3809) 2025-05-21 18:54:42 +08:00
9190db1099 feat: fix bug that token endpoint doesn't return 400/401 when type is object.TokenError (#3808) 2025-05-20 10:39:55 +08:00
1173f75794 feat: return HTTP status 400 instead of 200 in GetOAuthToken() (#3807) 2025-05-20 01:05:43 +08:00
086859d1ce feat: change User.Avatar length back to 500 2025-05-18 09:47:56 +08:00
9afaf5d695 feat: increase User.Avatar length to 1000 2025-05-17 19:59:17 +08:00
521f90a603 feat: fix access_token endpoint cannot read clientId in form when using device code flow (#3800) 2025-05-17 18:53:38 +08:00
4260efcfd0 feat: add useIdAsName field for WeCom OAuth provider (#3797) 2025-05-17 02:27:06 +08:00
d772b0b7a8 feat: fix bug that username will be random with useEmailAsUsername enabled (#3793) 2025-05-16 18:40:50 +08:00
702b390da1 feat: fix MFA preference doesn't work bug (#3790) 2025-05-15 21:04:36 +08:00
b15b3b9335 feat: support adapter in app.conf logConfig (#3784) 2025-05-14 08:27:11 +08:00
f8f864c5b9 feat: add logged-in IDP provider info to access token (#3776) 2025-05-11 09:51:51 +08:00
32 changed files with 321 additions and 157 deletions

View File

@ -31,7 +31,7 @@ radiusServerPort = 1812
radiusDefaultOrganization = "built-in" radiusDefaultOrganization = "built-in"
radiusSecret = "secret" radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1} quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"} logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false initDataNewOnly = false
initDataFile = "./init_data.json" initDataFile = "./init_data.json"
frontendBaseDir = "../cc_0" frontendBaseDir = "../cc_0"

View File

@ -115,7 +115,7 @@ func TestGetConfigLogs(t *testing.T) {
description string description string
expected string expected string
}{ }{
{"Default log config", `{"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`}, {"Default log config", `{"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
} }
err := beego.LoadAppConfig("ini", "app.conf") err := beego.LoadAppConfig("ini", "app.conf")

View File

@ -147,7 +147,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
c.ResponseError(c.T("auth:Challenge method should be S256")) c.ResponseError(c.T("auth:Challenge method should be S256"))
return return
} }
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage()) code, err := object.GetOAuthCode(userId, clientId, form.Provider, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil { if err != nil {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
return return

View File

@ -15,6 +15,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -78,12 +79,12 @@ func (c *ApiController) GetGroups() {
} }
for _, group := range groups { for _, group := range groups {
_, ok := groupsHaveChildrenMap[group.Name] _, ok := groupsHaveChildrenMap[group.GetId()]
if ok { if ok {
group.HaveChildren = true group.HaveChildren = true
} }
parent, ok := groupsHaveChildrenMap[group.ParentId] parent, ok := groupsHaveChildrenMap[fmt.Sprintf("%s/%s", group.Owner, group.ParentId)]
if ok { if ok {
group.ParentName = parent.DisplayName group.ParentName = parent.DisplayName
} }

View File

@ -177,10 +177,6 @@ func (c *ApiController) GetOAuthToken() {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth() clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
} }
if grantType == "urn:ietf:params:oauth:grant-type:device_code" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" { if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
// If clientId is empty, try to read data from RequestBody // If clientId is empty, try to read data from RequestBody
var tokenRequest TokenRequest var tokenRequest TokenRequest
@ -228,30 +224,36 @@ func (c *ApiController) GetOAuthToken() {
if deviceCode != "" { if deviceCode != "" {
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode) deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
if !ok { if !ok {
c.Data["json"] = object.TokenError{ c.Data["json"] = &object.TokenError{
Error: "expired_token", Error: "expired_token",
ErrorDescription: "token is expired", ErrorDescription: "token is expired",
} }
c.SetTokenErrorHttpStatus()
c.ServeJSON() c.ServeJSON()
c.SetTokenErrorHttpStatus()
return return
} }
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache) deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
if !deviceAuthCacheCast.UserSignIn { if !deviceAuthCacheCast.UserSignIn {
c.Data["json"] = object.TokenError{ c.Data["json"] = &object.TokenError{
Error: "authorization_pending", Error: "authorization_pending",
ErrorDescription: "authorization pending", ErrorDescription: "authorization pending",
} }
c.SetTokenErrorHttpStatus()
c.ServeJSON() c.ServeJSON()
c.SetTokenErrorHttpStatus()
return return
} }
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) { if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
c.Data["json"] = object.TokenError{ c.Data["json"] = &object.TokenError{
Error: "expired_token", Error: "expired_token",
ErrorDescription: "token is expired", ErrorDescription: "token is expired",
} }
c.SetTokenErrorHttpStatus()
c.ServeJSON() c.ServeJSON()
c.SetTokenErrorHttpStatus()
return return
} }
object.DeviceAuthMap.Delete(deviceCode) object.DeviceAuthMap.Delete(deviceCode)

View File

@ -703,7 +703,7 @@ func (c *ApiController) RemoveUserFromGroup() {
return return
} }
affected, err := object.DeleteGroupForUser(util.GetId(owner, name), groupName) affected, err := object.DeleteGroupForUser(util.GetId(owner, name), util.GetId(owner, groupName))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -27,8 +27,7 @@ type SmtpEmailProvider struct {
} }
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider { func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider {
dialer := &gomail.Dialer{} dialer := gomail.NewDialer(host, port, userName, password)
dialer = gomail.NewDialer(host, port, userName, password)
if typ == "SUBMAIL" { if typ == "SUBMAIL" {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true} dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} }

View File

@ -1,9 +1,9 @@
{ {
"account": { "account": {
"Failed to add user": "Gagal menambahkan pengguna", "Failed to add user": "Gagal menambahkan pengguna",
"Get init score failed, error: %w": "Gagal mendapatkan nilai init, kesalahan: %w", "Get init score failed, error: %w": "Gagal mendapatkan nilai inisiasi, kesalahan: %w",
"Please sign out first": "Silakan keluar terlebih dahulu", "Please sign out first": "Silakan keluar terlebih dahulu",
"The application does not allow to sign up new account": "Aplikasi tidak memperbolehkan untuk mendaftar akun baru" "The application does not allow to sign up new account": "Aplikasi tidak memperbolehkan pendaftaran akun baru"
}, },
"auth": { "auth": {
"Challenge method should be S256": "Metode tantangan harus S256", "Challenge method should be S256": "Metode tantangan harus S256",
@ -13,17 +13,17 @@
"State expected: %s, but got: %s": "Diharapkan: %s, tapi diperoleh: %s", "State expected: %s, but got: %s": "Diharapkan: %s, tapi diperoleh: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru melalui %%s, silakan gunakan cara lain untuk mendaftar", "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru melalui %%s, silakan gunakan cara lain untuk mendaftar",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru, silakan hubungi dukungan IT Anda", "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru, silakan hubungi dukungan IT Anda",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akun untuk provider: %s dan username: %s (%s) sudah terhubung dengan akun lain: %s (%s)", "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akun untuk penyedia: %s dan username: %s (%s) sudah terhubung dengan akun lain: %s (%s)",
"The application: %s does not exist": "Aplikasi: %s tidak ada", "The application: %s does not exist": "Aplikasi: %s tidak ada",
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application", "The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application",
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application", "The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application",
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application", "The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application", "The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
"The login method: login with password is not enabled for the application": "Metode login: login dengan kata sandi tidak diaktifkan untuk aplikasi tersebut", "The login method: login with password is not enabled for the application": "Metode login: login dengan sandi tidak diaktifkan untuk aplikasi tersebut",
"The organization: %s does not exist": "The organization: %s does not exist", "The organization: %s does not exist": "The organization: %s does not exist",
"The provider: %s is not enabled for the application": "Penyedia: %s tidak diaktifkan untuk aplikasi ini", "The provider: %s is not enabled for the application": "Penyedia: %s tidak diaktifkan untuk aplikasi ini",
"Unauthorized operation": "Operasi tidak sah", "Unauthorized operation": "Operasi tidak sah",
"Unknown authentication type (not password or provider), form = %s": "Jenis otentikasi tidak diketahui (bukan kata sandi atau pemberi), formulir = %s", "Unknown authentication type (not password or provider), form = %s": "Jenis otentikasi tidak diketahui (bukan sandi atau penyedia), formulir = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags", "User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing" "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
}, },
@ -39,59 +39,59 @@
"Email cannot be empty": "Email tidak boleh kosong", "Email cannot be empty": "Email tidak boleh kosong",
"Email is invalid": "Email tidak valid", "Email is invalid": "Email tidak valid",
"Empty username.": "Nama pengguna kosong.", "Empty username.": "Nama pengguna kosong.",
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in", "Face data does not exist, cannot log in": "Data wajah tidak ada, tidak bisa login",
"Face data mismatch": "Face data mismatch", "Face data mismatch": "Ketidakcocokan data wajah",
"FirstName cannot be blank": "Nama depan tidak boleh kosong", "FirstName cannot be blank": "Nama depan tidak boleh kosong",
"Invitation code cannot be blank": "Invitation code cannot be blank", "Invitation code cannot be blank": "Kode undangan tidak boleh kosong",
"Invitation code exhausted": "Invitation code exhausted", "Invitation code exhausted": "Kode undangan habis",
"Invitation code is invalid": "Invitation code is invalid", "Invitation code is invalid": "Kode undangan tidak valid",
"Invitation code suspended": "Invitation code suspended", "Invitation code suspended": "Kode undangan ditangguhkan",
"LDAP user name or password incorrect": "Nama pengguna atau kata sandi Ldap salah", "LDAP user name or password incorrect": "Nama pengguna atau sandi LDAP salah",
"LastName cannot be blank": "Nama belakang tidak boleh kosong", "LastName cannot be blank": "Nama belakang tidak boleh kosong",
"Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server ldap Anda", "Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server LDAP Anda",
"Organization does not exist": "Organisasi tidak ada", "Organization does not exist": "Organisasi tidak ada",
"Phone already exists": "Telepon sudah ada", "Phone already exists": "Telepon sudah ada",
"Phone cannot be empty": "Telepon tidak boleh kosong", "Phone cannot be empty": "Telepon tidak boleh kosong",
"Phone number is invalid": "Nomor telepon tidak valid", "Phone number is invalid": "Nomor telepon tidak valid",
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code", "Please register using the email corresponding to the invitation code": "Silakan mendaftar menggunakan email yang sesuai dengan kode undangan",
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code", "Please register using the phone corresponding to the invitation code": "Silakan mendaftar menggunakan email yang sesuai dengan kode undangan",
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code", "Please register using the username corresponding to the invitation code": "Silakan mendaftar menggunakan username yang sesuai dengan kode undangan",
"Session outdated, please login again": "Sesi kedaluwarsa, silakan masuk lagi", "Session outdated, please login again": "Sesi kadaluwarsa, silakan masuk lagi",
"The invitation code has already been used": "The invitation code has already been used", "The invitation code has already been used": "Kode undangan sudah digunakan",
"The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator", "The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server", "The user: %s doesn't exist in LDAP server": "Pengguna: %s tidak ada di server LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Nama pengguna hanya bisa menggunakan karakter alfanumerik, garis bawah atau tanda hubung, tidak boleh memiliki dua tanda hubung atau garis bawah berurutan, dan tidak boleh diawali atau diakhiri dengan tanda hubung atau garis bawah.", "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Nama pengguna hanya bisa menggunakan karakter alfanumerik, garis bawah atau tanda hubung, tidak boleh memiliki dua tanda hubung atau garis bawah berurutan, dan tidak boleh diawali atau diakhiri dengan tanda hubung atau garis bawah.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex", "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Nilai \\\"%s\\\" pada bidang akun \\\"%s\\\" tidak cocok dengan ketentuan",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"", "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Nilai \\\"%s\\\" pada bidang pendaftaran \\\"%s\\\" tidak cocok dengan ketentuan aplikasi \\\"%s\\\"",
"Username already exists": "Nama pengguna sudah ada", "Username already exists": "Nama pengguna sudah ada",
"Username cannot be an email address": "Username tidak bisa menjadi alamat email", "Username cannot be an email address": "Username tidak bisa menjadi alamat email",
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi", "Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka", "Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).", "Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter", "Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi", "You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan sandi atau kode yang salah terlalu sering, mohon tunggu selama %d menit lalu coba kembali",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon", "Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
"password or code is incorrect": "password or code is incorrect", "password or code is incorrect": "kata sandi atau kode salah",
"password or code is incorrect, you have %d remaining chances": "Kata sandi atau kode salah, Anda memiliki %d kesempatan tersisa", "password or code is incorrect, you have %d remaining chances": "Sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
"unsupported password type: %s": "jenis sandi tidak didukung: %s" "unsupported password type: %s": "jenis sandi tidak didukung: %s"
}, },
"general": { "general": {
"Missing parameter": "Parameter hilang", "Missing parameter": "Parameter hilang",
"Please login first": "Silahkan login terlebih dahulu", "Please login first": "Silahkan login terlebih dahulu",
"The organization: %s should have one application at least": "The organization: %s should have one application at least", "The organization: %s should have one application at least": "Organisasi: %s setidaknya harus memiliki satu aplikasi",
"The user: %s doesn't exist": "Pengguna: %s tidak ada", "The user: %s doesn't exist": "Pengguna: %s tidak ada",
"don't support captchaProvider: ": "Jangan mendukung captchaProvider:", "don't support captchaProvider: ": "Jangan mendukung captchaProvider:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode", "this operation is not allowed in demo mode": "tindakan ini tidak diizinkan pada mode demo",
"this operation requires administrator to perform": "this operation requires administrator to perform" "this operation requires administrator to perform": "tindakan ini membutuhkan peran administrator"
}, },
"ldap": { "ldap": {
"Ldap server exist": "Server ldap ada" "Ldap server exist": "Server ldap ada"
}, },
"link": { "link": {
"Please link first": "Tolong tautkan terlebih dahulu", "Please link first": "Silahkan tautkan terlebih dahulu",
"This application has no providers": "Aplikasi ini tidak memiliki penyedia", "This application has no providers": "Aplikasi ini tidak memiliki penyedia",
"This application has no providers of type": " Aplikasi ini tidak memiliki penyedia tipe ", "This application has no providers of type": " Aplikasi ini tidak memiliki penyedia tipe ",
"This provider can't be unlinked": "Pemberi layanan ini tidak dapat dipisahkan", "This provider can't be unlinked": "Penyedia layanan ini tidak dapat dipisahkan",
"You are not the global admin, you can't unlink other users": "Anda bukan admin global, Anda tidak dapat memutuskan tautan pengguna lain", "You are not the global admin, you can't unlink other users": "Anda bukan admin global, Anda tidak dapat memutuskan tautan pengguna lain",
"You can't unlink yourself, you are not a member of any application": "Anda tidak dapat memutuskan tautan diri sendiri, karena Anda bukan anggota dari aplikasi apa pun" "You can't unlink yourself, you are not a member of any application": "Anda tidak dapat memutuskan tautan diri sendiri, karena Anda bukan anggota dari aplikasi apa pun"
}, },
@ -101,11 +101,11 @@
"Unknown modify rule %s.": "Aturan modifikasi tidak diketahui %s." "Unknown modify rule %s.": "Aturan modifikasi tidak diketahui %s."
}, },
"permission": { "permission": {
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist" "The permission: \\\"%s\\\" doesn't exist": "Izin: \\\"%s\\\" tidak ada"
}, },
"provider": { "provider": {
"Invalid application id": "ID aplikasi tidak valid", "Invalid application id": "ID aplikasi tidak valid",
"the provider: %s does not exist": "provider: %s tidak ada" "the provider: %s does not exist": "penyedia: %s tidak ada"
}, },
"resource": { "resource": {
"User is nil for tag: avatar": "Pengguna kosong untuk tag: avatar", "User is nil for tag: avatar": "Pengguna kosong untuk tag: avatar",
@ -129,13 +129,13 @@
"token": { "token": {
"Grant_type: %s is not supported in this application": "Jenis grant (grant_type) %s tidak didukung dalam aplikasi ini", "Grant_type: %s is not supported in this application": "Jenis grant (grant_type) %s tidak didukung dalam aplikasi ini",
"Invalid application or wrong clientSecret": "Aplikasi tidak valid atau clientSecret salah", "Invalid application or wrong clientSecret": "Aplikasi tidak valid atau clientSecret salah",
"Invalid client_id": "Invalid client_id = ID klien tidak valid", "Invalid client_id": "ID klien tidak valid",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI pengalihan: %s tidak ada dalam daftar URI Pengalihan yang diizinkan", "Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI pengalihan: %s tidak ada dalam daftar URI Pengalihan yang diizinkan",
"Token not found, invalid accessToken": "Token tidak ditemukan, accessToken tidak valid" "Token not found, invalid accessToken": "Token tidak ditemukan, accessToken tidak valid"
}, },
"user": { "user": {
"Display name cannot be empty": "Nama tampilan tidak boleh kosong", "Display name cannot be empty": "Nama tampilan tidak boleh kosong",
"New password cannot contain blank space.": "Kata sandi baru tidak boleh mengandung spasi kosong." "New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong."
}, },
"user_upload": { "user_upload": {
"Failed to import users": "Gagal mengimpor pengguna" "Failed to import users": "Gagal mengimpor pengguna"
@ -148,16 +148,16 @@
"verification": { "verification": {
"Invalid captcha provider.": "Penyedia captcha tidak valid.", "Invalid captcha provider.": "Penyedia captcha tidak valid.",
"Phone number is invalid in your region %s": "Nomor telepon tidak valid di wilayah anda %s", "Phone number is invalid in your region %s": "Nomor telepon tidak valid di wilayah anda %s",
"The verification code has not been sent yet!": "The verification code has not been sent yet!", "The verification code has not been sent yet!": "Kode verifikasi belum terkirim!",
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!", "The verification code has not been sent yet, or has already been used!": "Kode verifikasi belum dikirim atau telah digunakan!",
"Turing test failed.": "Tes Turing gagal.", "Turing test failed.": "Tes Turing gagal.",
"Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.", "Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.",
"Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.", "Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.",
"Unknown type": "Tipe tidak diketahui", "Unknown type": "Tipe tidak diketahui",
"Wrong verification code!": "Kode verifikasi salah!", "Wrong verification code!": "Kode verifikasi salah!",
"You should verify your code in %d min!": "Anda harus memverifikasi kode Anda dalam %d menit!", "You should verify your code in %d min!": "Anda harus memverifikasi kode Anda dalam %d menit!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s", "please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "silahkan tambahkan penyedia SMS ke daftar \\\"Penyedia\\\" untuk aplikasi: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s", "please add an Email provider to the \\\"Providers\\\" list for the application: %s": "silahkan tambahkan penyedia Email ke daftar \\\"Penyedia\\\" untuk aplikasi: %s",
"the user does not exist, please sign up first": "Pengguna tidak ada, silakan daftar terlebih dahulu" "the user does not exist, please sign up first": "Pengguna tidak ada, silakan daftar terlebih dahulu"
}, },
"webauthn": { "webauthn": {

View File

@ -278,9 +278,16 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Session: &naver.Session{}, Session: &naver.Session{},
} }
case "Nextcloud": case "Nextcloud":
idp = GothIdProvider{ if hostUrl != "" {
Provider: nextcloud.New(clientId, clientSecret, redirectUrl), idp = GothIdProvider{
Session: &nextcloud.Session{}, Provider: nextcloud.NewCustomisedDNS(clientId, clientSecret, redirectUrl, hostUrl),
Session: &nextcloud.Session{},
}
} else {
idp = GothIdProvider{
Provider: nextcloud.New(clientId, clientSecret, redirectUrl),
Session: &nextcloud.Session{},
}
} }
case "OneDrive": case "OneDrive":
idp = GothIdProvider{ idp = GothIdProvider{

View File

@ -44,6 +44,7 @@ type ProviderInfo struct {
AppId string AppId string
HostUrl string HostUrl string
RedirectUrl string RedirectUrl string
DisableSsl bool
TokenURL string TokenURL string
AuthURL string AuthURL string
@ -79,9 +80,9 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "WeCom": case "WeCom":
if idpInfo.SubType == "Internal" { if idpInfo.SubType == "Internal" {
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
} else if idpInfo.SubType == "Third-party" { } else if idpInfo.SubType == "Third-party" {
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
} else { } else {
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType) return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
} }

View File

@ -29,13 +29,16 @@ import (
type WeComInternalIdProvider struct { type WeComInternalIdProvider struct {
Client *http.Client Client *http.Client
Config *oauth2.Config Config *oauth2.Config
UseIdAsName bool
} }
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComInternalIdProvider { func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComInternalIdProvider {
idp := &WeComInternalIdProvider{} idp := &WeComInternalIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl) config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config idp.Config = config
idp.UseIdAsName = useIdAsName
return idp return idp
} }
@ -169,5 +172,9 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
userInfo.Id = userInfo.Username userInfo.Id = userInfo.Username
} }
if idp.UseIdAsName {
userInfo.Username = userInfo.Id
}
return &userInfo, nil return &userInfo, nil
} }

View File

@ -28,13 +28,16 @@ import (
type WeComIdProvider struct { type WeComIdProvider struct {
Client *http.Client Client *http.Client
Config *oauth2.Config Config *oauth2.Config
UseIdAsName bool
} }
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComIdProvider { func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComIdProvider {
idp := &WeComIdProvider{} idp := &WeComIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl) config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config idp.Config = config
idp.UseIdAsName = useIdAsName
return idp return idp
} }
@ -183,6 +186,10 @@ func (idp *WeComIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
DisplayName: wecomUserInfo.UserInfo.Name, DisplayName: wecomUserInfo.UserInfo.Name,
AvatarUrl: wecomUserInfo.UserInfo.Avatar, AvatarUrl: wecomUserInfo.UserInfo.Avatar,
} }
if idp.UseIdAsName {
userInfo.Username = userInfo.Id
}
return &userInfo, nil return &userInfo, nil
} }

19
main.go
View File

@ -15,6 +15,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/beego/beego" "github.com/beego/beego"
@ -77,10 +78,26 @@ func main() {
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30 beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30
// beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode // beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode
err := logs.SetLogger(logs.AdapterFile, conf.GetConfigString("logConfig")) var logAdapter string
logConfigMap := make(map[string]interface{})
err := json.Unmarshal([]byte(conf.GetConfigString("logConfig")), &logConfigMap)
if err != nil { if err != nil {
panic(err) panic(err)
} }
_, ok := logConfigMap["adapter"]
if !ok {
logAdapter = "file"
} else {
logAdapter = logConfigMap["adapter"].(string)
}
if logAdapter == "console" {
logs.Reset()
}
err = logs.SetLogger(logAdapter, conf.GetConfigString("logConfig"))
if err != nil {
panic(err)
}
port := beego.AppConfig.DefaultInt("httpport", 8000) port := beego.AppConfig.DefaultInt("httpport", 8000)
// logs.SetLevel(logs.LevelInformational) // logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false) logs.SetLogFuncCall(false)

View File

@ -95,12 +95,13 @@ func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
} }
} }
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren) err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("name").In("name", groupIds).Find(&groupsHaveChildren)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, group := range groupsHaveChildren { for _, group := range groupsHaveChildren {
resultMap[group.ParentId] = groupMap[group.ParentId] resultMap[group.GetId()] = group
} }
return resultMap, nil return resultMap, nil
} }

View File

@ -475,6 +475,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
AuthURL: provider.CustomAuthUrl, AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl, UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping, UserMapping: provider.UserMapping,
DisableSsl: provider.DisableSsl,
} }
if provider.Type == "WeChat" { if provider.Type == "WeChat" {

View File

@ -31,7 +31,8 @@ type Claims struct {
Tag string `json:"tag"` Tag string `json:"tag"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken // the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
Azp string `json:"azp,omitempty"` Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -46,6 +47,17 @@ type UserShort struct {
Phone string `xorm:"varchar(100) index" json:"phone"` Phone string `xorm:"varchar(100) index" json:"phone"`
} }
type UserStandard struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"preferred_username,omitempty"`
Id string `xorm:"varchar(100) index" json:"id"`
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
}
type UserWithoutThirdIdp struct { type UserWithoutThirdIdp struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -140,6 +152,7 @@ type ClaimsShort struct {
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"` Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -159,6 +172,7 @@ type ClaimsWithoutThirdIdp struct {
Tag string `json:"tag"` Tag string `json:"tag"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"` Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -176,6 +190,20 @@ func getShortUser(user *User) *UserShort {
return res return res
} }
func getStandardUser(user *User) *UserStandard {
res := &UserStandard{
Owner: user.Owner,
Name: user.Name,
Id: user.Id,
DisplayName: user.DisplayName,
Avatar: user.Avatar,
Email: user.Email,
Phone: user.Phone,
}
return res
}
func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp { func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
res := &UserWithoutThirdIdp{ res := &UserWithoutThirdIdp{
Owner: user.Owner, Owner: user.Owner,
@ -274,6 +302,7 @@ func getShortClaims(claims Claims) ClaimsShort {
Scope: claims.Scope, Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims, RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp, Azp: claims.Azp,
Provider: claims.Provider,
} }
return res return res
} }
@ -287,6 +316,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
Scope: claims.Scope, Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims, RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp, Azp: claims.Azp,
Provider: claims.Provider,
} }
return res return res
} }
@ -308,6 +338,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
res["tag"] = claims.Tag res["tag"] = claims.Tag
res["scope"] = claims.Scope res["scope"] = claims.Scope
res["azp"] = claims.Azp res["azp"] = claims.Azp
res["provider"] = claims.Provider
for _, field := range tokenField { for _, field := range tokenField {
userField := userValue.FieldByName(field) userField := userValue.FieldByName(field)
@ -342,7 +373,7 @@ func refineUser(user *User) *User {
return user return user
} }
func generateJwtToken(application *Application, user *User, nonce string, scope string, host string) (string, string, string, error) { func generateJwtToken(application *Application, user *User, provider string, nonce string, scope string, host string) (string, string, string, error) {
nowTime := time.Now() nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour) expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour) refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
@ -362,9 +393,10 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
TokenType: "access-token", TokenType: "access-token",
Nonce: nonce, Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info // FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag, Tag: user.Tag,
Scope: scope, Scope: scope,
Azp: application.ClientId, Azp: application.ClientId,
Provider: provider,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: originBackend, Issuer: originBackend,
Subject: user.Id, Subject: user.Id,

View File

@ -136,7 +136,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application, nil 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) { func GetOAuthCode(userId string, clientId string, provider string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
user, err := GetUser(userId) user, err := GetUser(userId)
if err != nil { if err != nil {
return nil, err return nil, err
@ -171,7 +171,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
if err != nil { if err != nil {
return nil, err return nil, err
} }
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, nonce, scope, host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -379,7 +379,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err return nil, err
} }
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host) newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
Error: EndpointError, Error: EndpointError,
@ -558,7 +558,7 @@ func GetPasswordToken(application *Application, username string, password string
return nil, nil, err return nil, nil, err
} }
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
if err != nil { if err != nil {
return nil, &TokenError{ return nil, &TokenError{
Error: EndpointError, Error: EndpointError,
@ -604,7 +604,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
Type: "application", Type: "application",
} }
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", scope, host) accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", scope, host)
if err != nil { if err != nil {
return nil, &TokenError{ return nil, &TokenError{
Error: EndpointError, Error: EndpointError,
@ -668,7 +668,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
return nil, err return nil, err
} }
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", nonce, scope, host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -775,7 +775,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
return nil, nil, err return nil, nil, err
} }
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host) accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", host)
if err != nil { if err != nil {
return nil, &TokenError{ return nil, &TokenError{
Error: EndpointError, Error: EndpointError,

View File

@ -23,7 +23,7 @@ import (
) )
type ClaimsStandard struct { type ClaimsStandard struct {
*UserShort *UserStandard
EmailVerified bool `json:"email_verified,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"` PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
@ -33,6 +33,7 @@ type ClaimsStandard struct {
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
Address OIDCAddress `json:"address,omitempty"` Address OIDCAddress `json:"address,omitempty"`
Azp string `json:"azp,omitempty"` Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -47,13 +48,14 @@ func getStreetAddress(user *User) string {
func getStandardClaims(claims Claims) ClaimsStandard { func getStandardClaims(claims Claims) ClaimsStandard {
res := ClaimsStandard{ res := ClaimsStandard{
UserShort: getShortUser(claims.User), UserStandard: getStandardUser(claims.User),
EmailVerified: claims.User.EmailVerified, EmailVerified: claims.User.EmailVerified,
TokenType: claims.TokenType, TokenType: claims.TokenType,
Nonce: claims.Nonce, Nonce: claims.Nonce,
Scope: claims.Scope, Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims, RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp, Azp: claims.Azp,
Provider: claims.Provider,
} }
res.Phone = "" res.Phone = ""

View File

@ -837,7 +837,7 @@ func AddUser(user *User) (bool, error) {
return false, fmt.Errorf("the user's owner and name should not be empty") return false, fmt.Errorf("the user's owner and name should not be empty")
} }
if CheckUsername(user.Name, "en") != "" { if CheckUsernameWithEmail(user.Name, "en") != "" {
user.Name = util.GetRandomName() user.Name = util.GetRandomName()
} }
@ -1117,6 +1117,17 @@ func ExtendUserWithRolesAndPermissions(user *User) (err error) {
} }
func DeleteGroupForUser(user string, group string) (bool, error) { func DeleteGroupForUser(user string, group string) (bool, error) {
userObj, err := GetUser(user)
if err != nil {
return false, err
}
userObj.Groups = util.DeleteVal(userObj.Groups, group)
_, err = updateUser(user, userObj, []string{"groups"})
if err != nil {
return false, err
}
return userEnforcer.DeleteGroupForUser(user, group) return userEnforcer.DeleteGroupForUser(user, group)
} }

View File

@ -81,62 +81,12 @@ func UploadUsers(owner string, path string) (bool, error) {
return false, err return false, err
} }
transUsers, err := StringArrayToUser(table)
if err != nil {
return false, err
}
newUsers := []*User{} newUsers := []*User{}
for index, line := range table { for _, user := range transUsers {
line := line
if index == 0 || parseLineItem(&line, 0) == "" {
continue
}
user := &User{
Owner: parseLineItem(&line, 0),
Name: parseLineItem(&line, 1),
CreatedTime: parseLineItem(&line, 2),
UpdatedTime: parseLineItem(&line, 3),
Id: parseLineItem(&line, 4),
Type: parseLineItem(&line, 5),
Password: parseLineItem(&line, 6),
PasswordSalt: parseLineItem(&line, 7),
DisplayName: parseLineItem(&line, 8),
FirstName: parseLineItem(&line, 9),
LastName: parseLineItem(&line, 10),
Avatar: parseLineItem(&line, 11),
PermanentAvatar: "",
Email: parseLineItem(&line, 12),
Phone: parseLineItem(&line, 13),
Location: parseLineItem(&line, 14),
Address: []string{parseLineItem(&line, 15)},
Affiliation: parseLineItem(&line, 16),
Title: parseLineItem(&line, 17),
IdCardType: parseLineItem(&line, 18),
IdCard: parseLineItem(&line, 19),
Homepage: parseLineItem(&line, 20),
Bio: parseLineItem(&line, 21),
Tag: parseLineItem(&line, 22),
Region: parseLineItem(&line, 23),
Language: parseLineItem(&line, 24),
Gender: parseLineItem(&line, 25),
Birthday: parseLineItem(&line, 26),
Education: parseLineItem(&line, 27),
Score: parseLineItemInt(&line, 28),
Karma: parseLineItemInt(&line, 29),
Ranking: parseLineItemInt(&line, 30),
IsDefaultAvatar: false,
IsOnline: parseLineItemBool(&line, 31),
IsAdmin: parseLineItemBool(&line, 32),
IsForbidden: parseLineItemBool(&line, 33),
IsDeleted: parseLineItemBool(&line, 34),
SignupApplication: parseLineItem(&line, 35),
Hash: "",
PreHash: "",
CreatedIp: parseLineItem(&line, 36),
LastSigninTime: parseLineItem(&line, 37),
LastSigninIp: parseLineItem(&line, 38),
Ldap: "",
Properties: map[string]string{},
DeletedTime: parseLineItem(&line, 39),
}
if _, ok := oldUserMap[user.GetId()]; !ok { if _, ok := oldUserMap[user.GetId()]; !ok {
newUsers = append(newUsers, user) newUsers = append(newUsers, user)
} }

View File

@ -19,12 +19,14 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -689,3 +691,103 @@ func IsAppUser(userId string) bool {
} }
return false return false
} }
func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error {
unmarshalValue := new(T)
err := json.Unmarshal([]byte(fieldString), unmarshalValue)
if err != nil {
return err
}
fvElem := fieldValue
fvElem.Set(reflect.ValueOf(*unmarshalValue))
return nil
}
func StringArrayToUser(stringArray [][]string) ([]*User, error) {
fieldNames := stringArray[0]
excelMap := []map[string]string{}
userFieldMap := map[string]int{}
reflectedUser := reflect.TypeOf(User{})
for i := 0; i < reflectedUser.NumField(); i++ {
userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i
}
for idx, field := range stringArray {
if idx == 0 {
continue
}
tempMap := map[string]string{}
for idx, val := range field {
tempMap[fieldNames[idx]] = val
}
excelMap = append(excelMap, tempMap)
}
users := []*User{}
var err error
for _, u := range excelMap {
user := User{}
reflectedUser := reflect.ValueOf(&user).Elem()
for k, v := range u {
if v == "" || v == "null" || v == "[]" || v == "{}" {
continue
}
fName := strings.ToLower(strings.ReplaceAll(k, "_", ""))
fieldIdx, ok := userFieldMap[fName]
if !ok {
continue
}
fv := reflectedUser.Field(fieldIdx)
if !fv.IsValid() {
continue
}
switch fv.Kind() {
case reflect.String:
fv.SetString(v)
continue
case reflect.Bool:
fv.SetBool(v == "1")
continue
case reflect.Int:
intVal, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
fv.SetInt(int64(intVal))
continue
}
switch fv.Type() {
case reflect.TypeOf([]string{}):
err = setReflectAttr[[]string](&fv, v)
case reflect.TypeOf([]*string{}):
err = setReflectAttr[[]*string](&fv, v)
case reflect.TypeOf([]*FaceId{}):
err = setReflectAttr[[]*FaceId](&fv, v)
case reflect.TypeOf([]*MfaProps{}):
err = setReflectAttr[[]*MfaProps](&fv, v)
case reflect.TypeOf([]*Role{}):
err = setReflectAttr[[]*Role](&fv, v)
case reflect.TypeOf([]*Permission{}):
err = setReflectAttr[[]*Permission](&fv, v)
case reflect.TypeOf([]ManagedAccount{}):
err = setReflectAttr[[]ManagedAccount](&fv, v)
case reflect.TypeOf([]MfaAccount{}):
err = setReflectAttr[[]MfaAccount](&fv, v)
case reflect.TypeOf([]webauthn.Credential{}):
err = setReflectAttr[[]webauthn.Credential](&fv, v)
}
if err != nil {
return nil, err
}
}
users = append(users, &user)
}
return users, nil
}

View File

@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", nil return "", nil
} }
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx)) code, err := object.GetOAuthCode(userId, clientId, "", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil { if err != nil {
return "", err return "", err
} else if code.Message != "" { } else if code.Message != "" {

View File

@ -454,6 +454,7 @@ class ApplicationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}> <Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>)
{ {
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>) Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
} }

View File

@ -692,23 +692,35 @@ class ProviderEditPage extends React.Component {
</Row> </Row>
{ {
this.state.provider.type !== "WeCom" ? null : ( this.state.provider.type !== "WeCom" ? null : (
<Row style={{marginTop: "20px"}} > <React.Fragment>
<Col style={{marginTop: "5px"}} span={2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} : <Col style={{marginTop: "5px"}} span={2}>
</Col> {Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
<Col span={22} > </Col>
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => { <Col span={22} >
this.updateProviderField("method", value); <Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
}}> this.updateProviderField("method", value);
{ }}>
[ {
{id: "Normal", name: i18next.t("provider:Normal")}, [
{id: "Silent", name: i18next.t("provider:Silent")}, {id: "Normal", name: i18next.t("provider:Normal")},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>) {id: "Silent", name: i18next.t("provider:Silent")},
} ].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
</Select> }
</Col> </Select>
</Row>) </Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Use id as name"), i18next.t("provider:Use id as name - Tooltip"))} :
</Col>
<Col span={22} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
</React.Fragment>)
} }
</React.Fragment> </React.Fragment>
) )
@ -938,7 +950,7 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" ? null : ( this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" && this.state.provider.type !== "Nextcloud" ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :

View File

@ -1616,7 +1616,7 @@ export function isDarkTheme(themeAlgorithm) {
function getPreferredMfaProp(mfaProps) { function getPreferredMfaProp(mfaProps) {
for (const i in mfaProps) { for (const i in mfaProps) {
if (mfaProps[i].isPreffered) { if (mfaProps[i].isPreferred) {
return mfaProps[i]; return mfaProps[i];
} }
} }

View File

@ -37,7 +37,7 @@ export function signup(values) {
} }
export function getEmailAndPhone(organization, username) { export function getEmailAndPhone(organization, username) {
return fetch(`${authConfig.serverUrl}/api/get-email-and-phone?organization=${organization}&username=${username}`, { return fetch(`${authConfig.serverUrl}/api/get-email-and-phone?organization=${organization}&username=${encodeURIComponent(username)}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -193,7 +193,11 @@ class AuthCallback extends React.Component {
const token = res.data; const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`); Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") { } else if (responseType === "link") {
const from = innerParams.get("from"); let from = innerParams.get("from");
const oauth = innerParams.get("oauth");
if (oauth) {
from += `?oauth=${oauth}`;
}
Setting.goToLinkSoftOrJumpSelf(this, from); Setting.goToLinkSoftOrJumpSelf(this, from);
} else if (responseType === "saml") { } else if (responseType === "saml") {
if (res.data2.method === "POST") { if (res.data2.method === "POST") {

View File

@ -194,8 +194,10 @@ class PromptPage extends React.Component {
const redirectUri = params.get("redirectUri"); const redirectUri = params.get("redirectUri");
const code = params.get("code"); const code = params.get("code");
const state = params.get("state"); const state = params.get("state");
const oauth = params.get("oauth");
if (redirectUri === null || code === null || state === null) { if (redirectUri === null || code === null || state === null) {
return ""; const signInUrl = sessionStorage.getItem("signinUrl");
return oauth === "true" ? signInUrl : "";
} }
return `${redirectUri}?code=${code}&state=${state}`; return `${redirectUri}?code=${code}&state=${state}`;
} }

View File

@ -402,6 +402,10 @@ export function getAuthUrl(application, provider, method, code) {
redirectUri = `${redirectOrigin}/api/callback`; redirectUri = `${redirectOrigin}/api/callback`;
} else if (provider.type === "Google" && provider.disableSsl) { } else if (provider.type === "Google" && provider.disableSsl) {
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read"; scope += "+https://www.googleapis.com/auth/user.phonenumbers.read";
} else if (provider.type === "Nextcloud") {
if (provider.domain) {
endpoint = `${provider.domain}/apps/oauth2/authorize`;
}
} }
if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "Facebook" if (provider.type === "Google" || provider.type === "GitHub" || provider.type === "Facebook"

View File

@ -195,8 +195,9 @@ class SignupPage extends React.Component {
if (authConfig.appName === application.name) { if (authConfig.appName === application.name) {
return "/result"; return "/result";
} else { } else {
const oAuthParams = Util.getOAuthGetParameters();
if (Setting.hasPromptPage(application)) { if (Setting.hasPromptPage(application)) {
return `/prompt/${application.name}`; return `/prompt/${application.name}?oauth=${oAuthParams !== null}`;
} else { } else {
return `/result/${application.name}`; return `/result/${application.name}`;
} }

View File

@ -124,7 +124,7 @@ export const PasswordModal = (props) => {
width={600} width={600}
> >
<Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}> <Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}>
{(hasOldPassword && !Setting.isAdminUser(account)) ? ( {(hasOldPassword && !Setting.isLocalAdminUser(account)) ? (
<Row style={{width: "100%", marginBottom: "20px"}}> <Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:Old Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setOldPassword(e.target.value)} /> <Input.Password addonBefore={i18next.t("user:Old Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setOldPassword(e.target.value)} />
</Row> </Row>

Binary file not shown.